Skip to content

Commit b6cbb9b

Browse files
committed
docs(naming): ship naming-style.md (no-ambiguous-names rule)
Codifies the naming rules surfaced by the v6 audit: - Core rule: every exported function name reads clearly at its call site WITHOUT the import path. The module namespace is invisible to the reader. - Verb-first for actions (parseJson, not jsonParse). - Plural matches the underlying type (urlSearchParamsAs, matches URLSearchParams). - Drop `get` only on internal probes, not on public singletons. - _internal.ts for private; flat siblings for public. - async is the canonical name; Sync suffix for the sync variant. Documents why Tier 2 audit items were skipped: dropping noun suffixes from readSecret / getSocketPath / readSocketLibPrimordialNames / arrayChunk would create ambiguous names at the call site. Node's fs.writeFileSync is the guiding precedent — the noun survives even when the namespace already implies it.
1 parent bcdb160 commit b6cbb9b

1 file changed

Lines changed: 114 additions & 0 deletions

File tree

docs/naming-style.md

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
# Naming style
2+
3+
How socket-lib names its exports. Internal: most rules are about the cost of
4+
ambiguity at the call site, not about saving characters at the import.
5+
6+
## Core rule: no ambiguous names
7+
8+
**Every exported function name must read clearly at its call site without
9+
relying on the import path for context.**
10+
11+
The module path is invisible at the call site. A reader looking at one line
12+
of code shouldn't have to scroll to the import block to understand what
13+
`read(...)` reads.
14+
15+
```ts
16+
// NO — what is being read?
17+
import { read, write, del } from '@socketsecurity/lib/secrets/keychain'
18+
read({ service, account }) // 200 LOC later: which read?
19+
20+
// YES — call site stands alone
21+
import { readSecret, writeSecret, deleteSecret } from '@socketsecurity/lib/secrets/keychain'
22+
readSecret({ service, account }) // obvious
23+
```
24+
25+
Node's stdlib follows the same rule: `fs.writeFileSync`, not `fs.writeSync`.
26+
The `file` survives even though `fs` is the namespace.
27+
28+
### When to strip the noun
29+
30+
Only when the resulting name is still self-describing:
31+
32+
- **Constructors / factories**`Spinner()`, not `createSpinner()`. The noun
33+
IS the thing being made.
34+
- **Domain-specific verbs**`stringify` in `json/`, not `toString`. The
35+
verb is uniquely meaningful in JSON-land.
36+
- **Direct mirror of a stdlib API**`paths/resolve.ts` exports `resolve`
37+
and `relative` to match `node:path.resolve()` / `path.relative()`. The
38+
mental model is "this is `path`, but ours."
39+
40+
## Verb-first for actions
41+
42+
For functions that DO things:
43+
44+
```
45+
parseJson not jsonParse
46+
parseJsonSafe not safeJsonParse
47+
toEditablePackageJson not pkgJsonToEditable
48+
```
49+
50+
The verb tells the reader the *kind* of operation; the noun says *what* it
51+
operates on.
52+
53+
## Plural matches the underlying type
54+
55+
When wrapping a class or noun, match its plurality:
56+
57+
```
58+
urlSearchParamsAsArray not urlSearchParamAsArray // matches URLSearchParams
59+
```
60+
61+
## Internal helpers: drop `get` prefix on probes
62+
63+
Internal `getNativeHash()``nativeHash()`. The `get` prefix is noise on a
64+
private memoizing-probe; the bare noun reads as "give me the native hash
65+
function."
66+
67+
This is **internal** only — public API still uses `get` where it disambiguates
68+
(e.g. `getDefaultLogger()` returns a singleton).
69+
70+
## Private vs public files
71+
72+
Within a module:
73+
74+
- `_internal.ts` — underscore-prefix, NOT part of public API. The dist export
75+
generator skips `dist/**/_*` so consumers can't import these even if they
76+
guess the path.
77+
- Flat siblings (`macos.ts`, `linux.ts`, `windows.ts`, `keychain.ts`, `rc.ts`)
78+
— these ARE the public surface. Each filename declares its primary export.
79+
80+
## Pairs: async + sync
81+
82+
When both shapes exist, the async is the canonical name and sync gets a
83+
suffix:
84+
85+
```
86+
readSecret / readSecretSync
87+
writeSecret / writeSecretSync
88+
parseJson / (no sync variant — it's already sync)
89+
```
90+
91+
Don't invert this (`readSecretAsync` is wrong — async is the default).
92+
93+
## Types co-located with primary function
94+
95+
`SpinnerOptions`, `ParseJsonOptions`, `WriteOptions` live in the same module
96+
as `Spinner` / `parseJson` / `writeSecret`. The `Options` suffix is
97+
load-bearing: it signals "config for the function with the same prefix."
98+
99+
Type for an instance shape gets `Instance` suffix when there's a name
100+
collision with a factory:
101+
102+
```
103+
function Spinner(opts): SpinnerInstance // factory and instance both named Spinner before
104+
```
105+
106+
## Variables in code
107+
108+
Local variables follow normal JS conventions (camelCase, descriptive). The
109+
strict rules above apply only to **exported** names.
110+
111+
## When in doubt
112+
113+
Read the call site out loud. If a reasonable reader needs to look up the
114+
import to understand it, the name is wrong.

0 commit comments

Comments
 (0)