Skip to content

Commit ee20b81

Browse files
committed
chore(sync): cascade predicate lift + docs/claude.md/ reorg from socket-repo-template
1 parent a02be4a commit ee20b81

8 files changed

Lines changed: 322 additions & 5 deletions

File tree

.config/oxlint-plugin/rules/prefer-undefined-over-null.js

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,97 @@ const rule = {
103103
return ['===', '!==', '==', '!='].includes(parent.operator)
104104
}
105105

106+
/**
107+
* `expect(x).toBe(null)` / `.toEqual(null)` / `.toStrictEqual(null)` /
108+
* `.toMatchObject(null)` — vitest/jest assertion matchers where the
109+
* `null` is the SEMANTIC value being asserted. Rewriting to
110+
* `undefined` flips the test contract (a passing test that asserted
111+
* "x is null" now asserts "x is undefined").
112+
*
113+
* Also covers chai (`.equal(null)` / `.equals(null)` / `.is(null)` /
114+
* `.same(null)`) and node:assert (`assert.equal(_, null)` /
115+
* `.deepEqual(_, null)` / `.deepStrictEqual(_, null)` /
116+
* `.strictEqual(_, null)`).
117+
*
118+
* The detection is shape-based, not name-import-based — any call
119+
* that ends in `.<assert-method>(null, ...)` qualifies. False
120+
* positives (a non-test method named `toBe`) are extremely rare;
121+
* the cost is missing a real autofix opportunity, which is a safe
122+
* outcome.
123+
*/
124+
const ASSERT_METHODS = new Set([
125+
'deepEqual',
126+
'deepStrictEqual',
127+
'equal',
128+
'equals',
129+
'is',
130+
'notDeepEqual',
131+
'notDeepStrictEqual',
132+
'notEqual',
133+
'notStrictEqual',
134+
'same',
135+
'strictEqual',
136+
'toBe',
137+
'toEqual',
138+
'toMatchObject',
139+
'toStrictEqual',
140+
])
141+
142+
function isAssertionLibraryArg(node) {
143+
const parent = unwrapTsCast(node)
144+
if (!parent || parent.type !== 'CallExpression') {
145+
return false
146+
}
147+
const callee = parent.callee
148+
if (
149+
callee.type !== 'MemberExpression' ||
150+
callee.property.type !== 'Identifier'
151+
) {
152+
return false
153+
}
154+
return ASSERT_METHODS.has(callee.property.name)
155+
}
156+
157+
/**
158+
* `const x: Foo | null = null` / `let y: Foo | null | undefined = null`
159+
* — the developer explicitly opted into null in the variable's
160+
* type signature. The dedicated annotation IS the contract;
161+
* flipping the value alone leaves the contract intact but
162+
* produces dead `undefined` writes against a `| null` slot.
163+
*
164+
* Faster than the generic `hasNullTypeAnnotation` walk-up
165+
* because it short-circuits at the immediate VariableDeclarator
166+
* parent. Both predicates are kept — this fast-path covers the
167+
* canonical declarator shape; the walk-up handles the broader
168+
* Property / Parameter / return-type / TS-cast cases that
169+
* declarator-only detection misses.
170+
*
171+
* Textual scan over `<id>: <annot> = ` rather than AST navigation:
172+
* the typeAnnotation field shape varies between oxlint AST and
173+
* babel/typescript-eslint AST, so the regex is the most resilient
174+
* detector across plugin host versions.
175+
*/
176+
function isNullableTypeInitializer(node) {
177+
const parent = node.parent
178+
if (!parent || parent.type !== 'VariableDeclarator') {
179+
return false
180+
}
181+
if (parent.init !== node) {
182+
return false
183+
}
184+
const declStart = parent.range
185+
? parent.range[0]
186+
: (parent.start ?? parent.id?.range?.[0])
187+
const litStart = node.range ? node.range[0] : node.start
188+
if (typeof declStart !== 'number' || typeof litStart !== 'number') {
189+
return false
190+
}
191+
const text = sourceCode.getText().slice(declStart, litStart)
192+
// Require `: <typeexpr>... null ... =` — colon (type annotation),
193+
// literal `null` token, then `=` (initializer separator).
194+
return /:[^=]*\bnull\b[^=]*=/.test(text)
195+
}
196+
106197
function isJsonStringifyReplacer(node) {
107198
// JSON.stringify(value, replacer, space) — `replacer` is conventionally null.
108199
const parent = unwrapTsCast(node)
@@ -277,6 +368,12 @@ const rule = {
277368
if (isJsonStringifyReplacer(node)) {
278369
return
279370
}
371+
if (isAssertionLibraryArg(node)) {
372+
return
373+
}
374+
if (isNullableTypeInitializer(node)) {
375+
return
376+
}
280377

281378
if (hasNullTypeAnnotation(node)) {
282379
// Surrounding type annotation mentions null — report without

CLAUDE.md

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -152,7 +152,7 @@ The principle: the working tree at end-of-turn should match the user's mental mo
152152

153153
### Hook bypasses require the canonical phrase
154154

155-
🚨 Reverting tracked changes or bypassing a hook (--no-verify, DISABLE*PRECOMMIT*\*, --no-gpg-sign, force-push) requires the user to type **`Allow <X> bypass`** verbatim in a recent user turn (e.g. `Allow revert bypass`, `Allow no-verify bypass`). Paraphrases don't count. Enforced by `.claude/hooks/no-revert-guard/`. Full phrase table: [`docs/references/bypass-phrases.md`](docs/references/bypass-phrases.md).
155+
🚨 Reverting tracked changes or bypassing a hook (--no-verify, DISABLE*PRECOMMIT*\*, --no-gpg-sign, force-push) requires the user to type **`Allow <X> bypass`** verbatim in a recent user turn (e.g. `Allow revert bypass`, `Allow no-verify bypass`). Paraphrases don't count. Enforced by `.claude/hooks/no-revert-guard/`. Full phrase table: [`docs/claude.md/bypass-phrases.md`](docs/claude.md/bypass-phrases.md).
156156

157157
### Variant analysis on every High/Critical finding
158158

@@ -191,6 +191,10 @@ How to check:
191191

192192
Never silently let drift sit. Either reconcile in the same PR or open a follow-up PR titled `chore(sync): cascade <thing> from <newer-repo>` and link it.
193193

194+
### Never fork fleet-canonical files locally
195+
196+
🚨 Fleet-canonical files (anything tracked by `socket-repo-template/scripts/sync-scaffolding/manifest.mts`) MUST be edited in `socket-repo-template/template/...` and cascaded out — never branched locally in a downstream fleet repo. If you spot a useful predicate / helper / test / behavior in a fleet-canonical file in a downstream repo that's NOT in the template, that's a bug — lift it up first, then re-cascade. Full canonical-surface list + lifting workflow: [`docs/claude.md/no-local-fork-canonical.md`](docs/claude.md/no-local-fork-canonical.md).
197+
194198
### Code style
195199

196200
- **Comments** — default to none. Write one only when the WHY is non-obvious to a senior engineer. **When you do write a comment, the audience is a junior dev**: explain the constraint, the hidden invariant, the "why this and not the obvious thing." Don't label it ("for junior devs:", "intuition:", etc.) — just write in that voice. No teacher-tone, no condescension, no flattering the reader.
@@ -204,8 +208,8 @@ Never silently let drift sit. Either reconcile in the same PR or open a follow-u
204208
- **File deletion** — route every delete through `safeDelete()` / `safeDeleteSync()` from `@socketsecurity/lib/fs`. Never `fs.rm` / `fs.unlink` / `fs.rmdir` / `rm -rf` directly — even for one known file. Prefer the async `safeDelete()` over `safeDeleteSync()` when the surrounding code is already async (test bodies, request handlers, build scripts that await elsewhere) — sync I/O blocks the event loop and there's no benefit when the caller is awaiting anyway. Reserve `safeDeleteSync()` for top-level scripts whose entire flow is sync.
205209
- **Edits** — Edit tool, never `sed` / `awk`.
206210
- **Generated reports** — quality scans, security audits, perf snapshots, anything an automated tool emits — write to `.claude/reports/` (naturally gitignored as part of `.claude/*`, no separate rule needed). Never commit reports to a tracked `reports/`, `docs/reports/`, or similarly-named tracked directory: dated reports rot the moment they land and the directory becomes a graveyard. The current state of the repo is the report; tools regenerate findings on demand. If a finding is genuinely worth keeping past one run, fix it or open an issue — don't pickle it as a markdown file.
207-
- **Inclusive language** — see [`docs/references/inclusive-language.md`](docs/references/inclusive-language.md) for the substitution table.
208-
- **Sorting** — sort alphanumerically (literal byte order, ASCII before letters). Applies to: object property keys (config + return shapes + internal state — `__proto__: null` first); named imports inside a single statement (`import { a, b, c }`); `Set` / `SafeSet` constructor arguments; allowlists / denylists / config arrays / interface members. Position-bearing arrays (where index matters) keep their meaningful order. Full details in [`docs/references/sorting.md`](docs/references/sorting.md). When in doubt, sort.
211+
- **Inclusive language** — see [`docs/claude.md/inclusive-language.md`](docs/claude.md/inclusive-language.md) for the substitution table.
212+
- **Sorting** — sort alphanumerically (literal byte order, ASCII before letters). Applies to: object property keys (config + return shapes + internal state — `__proto__: null` first); named imports inside a single statement (`import { a, b, c }`); `Set` / `SafeSet` constructor arguments; allowlists / denylists / config arrays / interface members. Position-bearing arrays (where index matters) keep their meaningful order. Full details in [`docs/claude.md/sorting.md`](docs/claude.md/sorting.md). When in doubt, sort.
209213
- **`Promise.race` / `Promise.any` in loops** — never re-race a pool that survives across iterations (the handlers stack). See `.claude/skills/plug-leaking-promise-race/SKILL.md`.
210214
- **`Safe` suffix** — non-throwing wrappers end in `Safe` (`safeDelete`, `safeDeleteSync`, `applySafe`, `weakRefSafe`). Read it as "X, but safe from throwing." The wrapper traps the thrown value internally and returns `undefined` (or the documented fallback). Don't invent alternative suffixes (`Try`, `OrUndefined`, `Maybe`) — pick `Safe`.
211215
- **`node:smol-*` modules** — feature-detect, then require. From outside socket-btm (socket-lib, socket-cli, anywhere else): `import { isBuiltin } from 'node:module'; if (isBuiltin('node:smol-X')) { const mod = require('node:smol-X') }`. The `node:smol-*` namespace is provided by socket-btm's smol Node binary; on stock Node `isBuiltin` returns false and the require would throw. Wrap the loader in a `/*@__NO_SIDE_EFFECTS__*/` lazy-load that caches the result — see `socket-lib/src/smol/util.ts` and `socket-lib/src/smol/primordial.ts` for canonical shape. **Inside** socket-btm's `additions/source-patched/` JS (the smol binary's own bootstrap code), use `internalBinding('smol_X')` directly — that's the C++-binding access path and it's guaranteed available there.
@@ -275,7 +279,7 @@ An error message is UI. The reader should fix the problem from the message alone
275279
3. **Saw vs. wanted** — the bad value and the allowed shape or set.
276280
4. **Fix** — one imperative action (`rename the key to …`).
277281

278-
Use `isError` / `isErrnoException` / `errorMessage` / `errorStack` from `@socketsecurity/lib/errors` over hand-rolled checks. Use `joinAnd` / `joinOr` from `@socketsecurity/lib/arrays` for allowed-set lists. Full guidance in [`docs/references/error-messages.md`](docs/references/error-messages.md).
282+
Use `isError` / `isErrnoException` / `errorMessage` / `errorStack` from `@socketsecurity/lib/errors` over hand-rolled checks. Use `joinAnd` / `joinOr` from `@socketsecurity/lib/arrays` for allowed-set lists. Full guidance in [`docs/claude.md/error-messages.md`](docs/claude.md/error-messages.md).
279283

280284
### Token hygiene
281285

@@ -296,7 +300,7 @@ Full hook spec in [`.claude/hooks/token-guard/README.md`](.claude/hooks/token-gu
296300
- `/scanning-security` — AgentShield + zizmor audit
297301
- `/scanning-quality` — quality analysis
298302
- Shared subskills in `.claude/skills/_shared/`
299-
- **Handing off to another agent** — see [`docs/references/agent-delegation.md`](docs/references/agent-delegation.md) for when to reach for `codex:codex-rescue`, the `delegate` subagent (OpenCode → Fireworks/Synthetic/Kimi), `Explore`, `Plan`, vs. driving the skill CLIs directly. The CLI-subprocess contract used by skills lives in [`_shared/multi-agent-backends.md`](.claude/skills/_shared/multi-agent-backends.md).
303+
- **Handing off to another agent** — see [`docs/claude.md/agent-delegation.md`](docs/claude.md/agent-delegation.md) for when to reach for `codex:codex-rescue`, the `delegate` subagent (OpenCode → Fireworks/Synthetic/Kimi), `Explore`, `Plan`, vs. driving the skill CLIs directly. The CLI-subprocess contract used by skills lives in [`_shared/multi-agent-backends.md`](.claude/skills/_shared/multi-agent-backends.md).
300304

301305
#### Skill scope: fleet vs partial vs unique
302306

File renamed without changes.
File renamed without changes.

0 commit comments

Comments
 (0)