docs(spec): state both roles of aliases, not only the gap it fills - #17662
Conversation
`strictObject`'s `aliases` option was documented as a universal — "semantic near-misses edit distance cannot reach" — in the three places an adopter reads: the module docblock, the `StrictObjectOptions.aliases` JSDoc an editor shows on hover, and the same JSDoc on the published `strictUnknownKeyError`'s options interface. The word "cannot" denies the option's second role. The lookup asks the alias table first and reaches the distance matcher only when that misses, so an alias wins outright — and an entry is equally right when distance does reach the key and answers with the wrong one. The proving case ships in `kernel/manifest.zod.ts` — `hosts` is 2 edits from the declared `hooks` against a budget of `max(2, floor(5 / 3))` = 2, so the entry is what keeps a plugin author off lifecycle hooks on the block that also grants network access. Neither role is rare. Measured over every surface the `strictObject` registry records: 1910 alias entries, 1658 unreachable by distance and 252 reachable — 211 where the fallback would have answered identically, and 41 where it answers a different key the entry overrules. The correction carries that count rather than the hedge it replaces, and names the overruling role explicitly so an adopter holding a confidently wrong suggestion is not told to leave it alone. The ledger's own copy of the sentence moves with them. Prose only: no alias is added or removed, no schema, key list, strictness or author-visible message changes, and `visibleWhen` -> `visible` stays as the proving case for the gap half after re-measuring that it is still unreachable. Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MkQhmuuJAVDjmeWNixwDDH
📓 Docs Drift Check2 anchor(s) derived from 1 changed package(s); no hand-written page names any of them, so this run has nothing to list — not a clean bill of health. This check sees only pages that NAME a derived anchor: one that documents this change in prose, or enumerates it in an authoring dialect, names none and stays invisible to it on every run. What this run could not see
Coarse fallback — 135 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin f82811f35f534a9c2b66da231f32a58c45ec9d58 && git checkout f82811f35f534a9c2b66da231f32a58c45ec9d58
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin f19dbcf43a4f332ad7fd21defe08230dce638028 d67cab8d2cb1d0c510a89a2254d513882bba5bd9 && git checkout -B drift-repro f19dbcf43a4f332ad7fd21defe08230dce638028 && git merge --no-ff d67cab8d2cb1d0c510a89a2254d513882bba5bd9
node scripts/docs-audit/affected-docs.mjs --json f19dbcf43a4f332ad7fd21defe08230dce638028 |
Part of #17361
Prose only. The diff adds no key to any published payload — every changed line in the two TypeScript files is inside a block comment (proof below).
What was wrong
strictObject'saliaseswas documented as a universal — "semantic near-misses edit distance cannot reach" — so an adopter holding a near-miss that distance does reach, and answers wrongly, reads the authority and concludesaliasesis not their tool. That is the campaign's own finding-7 shape: this campaign's fix signposting the way into the failure mode it exists to kill.The mechanism the sentence denies: the lookup is
so an alias is consulted before the distance fallback and wins outright. It therefore has two jobs, not one:
visibleWhentovisiblehoststonetwork(pluginpermissions)max(2, floor(5/3))= 2,hostsis exactly 2 from the declaredhooksPrerequisite readings — anchored by content on
origin/maind07fc178b9, not by line and not from the cardProbe: whitespace flattened per file (a phrase can wrap across a line break),
edit[- ]?distance (cannot|can not|can.t) reachcase-insensitive,grep -o | wc -lovergit ls-files(tracked only, so no builtdist/in the population). 19 occurrences in 17 files. Dark control (a fabricated token) 0; lit controlstrictObject(383.StrictObjectOptions.aliasesJSDoc, both inpackages/spec/src/shared/strict-object.ts— 2 occurrences in that file.hosts: 'network'is live inpackages/spec/src/kernel/manifest.zod.ts. Distance computed independently, in a standalone Python Levenshtein against the four declared keysservices/hooks/network/fs:hosts-hooks= 2, budget = 2 — inside. Controls from the same run:filesystem-fs= 8 (budget 3),paths-fs= 4 (budget 2), both outside, so the instrument returns both answers.StrictUnknownKeyErrorOptions.aliasesinshared/suggestions.zod.ts— reached only because the site list was re-derived rather than trusted. Corrected here too.The census — the count, not a hedge
A correction that says aliases are usually unreachable, while more counter-examples ship, is a new false sentence. So every shipped alias was classified rather than sampled: the
strictObjectDeclarations()registry was forced over every module underpackages/spec/src(thealias-integrity.test.tswalk), and for each entry the realfindClosestMatcheswas run against that surface's ownknownKeysat that key's own budget.Controls on the instrument: lit —
visibleWhen(the docblock's own proving case) classifies, asGAP; dark — a fabricated alias token returns 0 rows. Source-side cross-check on tracked files only: 255aliases:occurrences in 64 files, all insidepackages/spec; no package outsidespeccallsstrictUnknownKeyErrorat all.⇒
hostsis row 20 of 41, not the only one. The corrected prose therefore names the overruling role as a role, and carries the measured count with its date rather than a word like "usually".What changed
packages/spec/src/shared/strict-object.ts— module docblock: thealiasesbullet now splits into the two roles, each with its proving case, plus the measured split and an explicit "do not read this as only for what distance cannot reach".packages/spec/src/shared/strict-object.ts—StrictObjectOptions.aliasesJSDoc (the hover text): same correction, short form.packages/spec/src/shared/suggestions.zod.ts—StrictUnknownKeyErrorOptions.aliasesJSDoc, the published factory's hover text (the re-derived fourth site).docs/audits/2026-07-unknown-key-strictness-ledger.md— the ledger's copy of the sentence. Its two cited examples (fromtosource,readtoallowRead) were re-measured as still unreachable and kept..changeset/strict-object-aliases-two-roles.md—patchon@objectstack/spec(a docblock ships in the published.d.ts).⛔ No alias added or removed, no behaviour change, no key list, strictness or author-visible message touched.
Verification
+/-line in the two TypeScript files matches a block-comment continuation; the detector fires on a synthetic code line (lit control), and finds nothing in the real diff.pnpm --filter @objectstack/spec build— exit 0, 34/34 declaration files.pnpm --filter @objectstack/spec check:generated— all 15 generated artifacts up to date (so nothing generated mirrors these docblocks; nothing was hand-edited).pnpm --filter @objectstack/spec exec vitest run src/shared/— 27 files, 567 tests, all pass.pnpm --filter @objectstack/spec typecheck— exit 0.eslint --no-inline-configover the whole repo — 6622 files, 0 errors (the union, not a narrowing).scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack: 76 derived, 74 run green, 0 unrun; reconciled with--rancarrying each recorded exit code. The 2 not measured arecheck:dual-build-cjs-loadsandcheck:lean-entry-closure— both exit 3, PREREQUISITE NOT MET (they read a whole-repo build). Exit 3 is a refusal, not a red; declared here and left to CI.#,Part of,Refs,Fixes,Closes,Resolves→ 0 each, with lit controlsCo-authored-by→ 1 andClaude-Session→ 1. The card relation is declared once, here in the body.Measurements above are on the final commit,
d67cab8d2c.验收备注
Noted, not filed — three per-site copies of the same sentence that my census measured false, each outside this card's declared file face and each a different judgement call:
packages/objectql/src/validation/record-validator.tsandpackages/spec/src/data/default-value-shape.tsboth cite "edit distance cannot reach it (latitudetolat)". Measured:latitudehas budget 2 and is 2 from the declaredaltitude, so the fallback reaches it and answersaltitude— the alias is an overrule, and the cited example is the one that proves the opposite of what it is cited for. Successor: whoever next touches the location/address value-shape prose.packages/spec/src/data/driver/turso.zod.ts— "Semantic near-misses only — the spellings edit distance cannot reach" sits above a table containinguritourl, which the fallback reaches at distance 1 and answers identically. Harmless entry, false comment. Successor: whoever next touches the turso datasource schema.ui/chart.zod.ts(both tables),data/object.zod.tsand the hedged "most of these" indata/authoring-key-lint.ts.⛔ Do not flip this out of draft, enqueue it, or arm auto-merge — the dispatching seat lands it.
Generated by Claude Code