From d67cab8d2cb1d0c510a89a2254d513882bba5bd9 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 11 Sep 2026 09:04:28 +0000 Subject: [PATCH] docs(spec): state both roles of `aliases`, not only the gap it fills MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `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 Claude-Session: https://claude.ai/code/session_01MkQhmuuJAVDjmeWNixwDDH --- .changeset/strict-object-aliases-two-roles.md | 15 +++++++ .../2026-07-unknown-key-strictness-ledger.md | 9 +++- packages/spec/src/shared/strict-object.ts | 42 ++++++++++++++++--- packages/spec/src/shared/suggestions.zod.ts | 12 ++++-- 4 files changed, 67 insertions(+), 11 deletions(-) create mode 100644 .changeset/strict-object-aliases-two-roles.md diff --git a/.changeset/strict-object-aliases-two-roles.md b/.changeset/strict-object-aliases-two-roles.md new file mode 100644 index 0000000000..b924bb8095 --- /dev/null +++ b/.changeset/strict-object-aliases-two-roles.md @@ -0,0 +1,15 @@ +--- +'@objectstack/spec': patch +--- + +Correct `aliases`' documented contract: it is not "only for what edit distance cannot reach". + +`strictObject`'s `aliases` option was documented as a universal in the three places an adopter reads — the module docblock in `shared/strict-object.ts`, the `StrictObjectOptions.aliases` JSDoc an editor shows on hover, and the same JSDoc on the published `strictUnknownKeyError`'s `StrictUnknownKeyErrorOptions.aliases` — all saying aliases are "semantic near-misses edit distance cannot reach". The word *cannot* denies the option's second job. + +The lookup is `aliases[aliasProbe(key)] ?? findClosestMatches(key, knownKeys, maxDistance, 1)[0]`: an alias is consulted **before** the distance fallback and wins outright. So an entry is equally right when distance *does* reach the key and answers with the wrong one — `hosts` is 2 edits from the declared `hooks` against a budget of `Math.max(2, Math.floor(5 / 3))` = 2, so on the plugin `permissions` block the entry is what keeps an author off lifecycle hooks. + +Neither role is rare, and the correction carries its own count rather than the hedge it replaces. Measured over every surface the `strictObject` registry records, 2026-09-11: **1910** alias entries, **1658** unreachable by distance, **252** reachable — 211 where the fallback would have answered identically, and **41** where it answers a different key the entry overrules. + +The failure mode the old sentence produced is precise and has a live carrier: an adopter with a reachable-but-wrong near-miss read "edit distance cannot reach", concluded `aliases` was not the tool for their case, and left the confident wrong suggestion in place. + +Prose only. No alias is added or removed, no schema, key list, strictness or error message changes, and `visibleWhen → visible` (verified still unreachable) stays as the proving case for the gap half. diff --git a/docs/audits/2026-07-unknown-key-strictness-ledger.md b/docs/audits/2026-07-unknown-key-strictness-ledger.md index 8e570be830..2f05f4687a 100644 --- a/docs/audits/2026-07-unknown-key-strictness-ledger.md +++ b/docs/audits/2026-07-unknown-key-strictness-ledger.md @@ -138,8 +138,13 @@ schema needs: lazySchema(() => strictObject({ surface, history, aliases?, guidance? }, { ...shape })) ``` -- `aliases` — semantic near-misses edit distance cannot reach (`visibleWhen` → - `visible`, `from` → `source`, `read` → `allowRead`). +- `aliases` — curated near-miss answers, consulted before the distance fallback + and preferred over it, so the table carries both the near-misses distance + **cannot reach** (`visibleWhen` → `visible`, `from` → `source`, `read` → + `allowRead`) and the ones it **reaches and answers wrongly** (`hosts` → + `network`, 2 edits from the declared `hooks` against a budget of 2 — #16859). + Measured 2026-09-11 (#17361): of 1910 entries, 1658 unreachable, 211 + reachable-and-agreeing, 41 overruling a wrong hit. - `guidance` — exact-key prescriptions: **tombstones for retired keys** (the rejection carries the upgrade — AGENTS.md Post-Task Checklist #3) and wrong-layer pointers (`apiOperations` is response-side; `objectName` belongs diff --git a/packages/spec/src/shared/strict-object.ts b/packages/spec/src/shared/strict-object.ts index 963b15686d..24c9e672f3 100644 --- a/packages/spec/src/shared/strict-object.ts +++ b/packages/spec/src/shared/strict-object.ts @@ -29,10 +29,30 @@ * `aliases` and `guidance` stay hand-written, because they are the part that * carries judgement rather than transcription: * - * - `aliases` — semantic near-misses edit distance cannot reach. The one that - * proves the category is `visibleWhen → visible`: ADR-0089 made `visibleWhen` - * the correct spelling on view/page, so an author borrowing it on a different - * surface is not making a typo, and only a human-written entry can catch it. + * - `aliases` — the curated answer for a semantic near-miss, a different *word* + * for the same intent. It is consulted BEFORE the distance fallback and wins + * outright (`aliases[aliasProbe(key)] ?? findClosestMatches(…)` in + * `suggestions.zod.ts`), which is what gives it **two** jobs, not one: + * - **filling a gap** — the near-miss distance cannot reach. The proving case + * is `visibleWhen → visible`: ADR-0089 made `visibleWhen` the correct + * spelling on view/page, so an author borrowing it on a different surface + * is not making a typo, and only a human-written entry can catch it. + * - **overruling a wrong hit** — the near-miss distance CAN reach, and + * answers with the wrong key. The proving case is `hosts → network` on the + * plugin `permissions` block (#16859): the budget is `max(2, len/3)` = 2 + * for a five-character key and `hosts` is exactly 2 from the declared + * `hooks`, so without the entry the author is sent to lifecycle hooks on + * the one block that also grants network access. + * + * ⚠️ Neither job is the rare one, and this bullet claimed only the first + * until #17361 measured it. Over every registered surface on 2026-09-11: + * 1910 alias entries, 1658 of them 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. ⛔ So do not + * read this option as *only* for what distance cannot reach: that reading + * tells an adopter holding a confident wrong suggestion — the case the + * option is most needed for — that `aliases` is not their tool, which is + * this campaign's own finding-7 shape (see {@link acceptsNothing}). * - `guidance` — tombstones for retired keys (the rejection must carry the * upgrade) and wrong-layer pointers. * @@ -114,8 +134,18 @@ export interface StrictObjectOptions { /** One sentence: what silently happened before this shape was closed. */ history: string; /** - * Semantic near-misses edit distance cannot reach — a different *word* for - * the same intent, usually correct on a neighbouring surface. + * Curated answers for semantic near-misses — a different *word* for the same + * intent, usually correct on a neighbouring surface. + * + * Looked up BEFORE the edit-distance fallback and preferred over it, so an + * entry is the right tool in **both** directions: the word distance cannot + * reach (`visibleWhen → visible`), and the word distance *does* reach and + * gets WRONG (`hosts → network`, where `hosts` is 2 edits from the declared + * `hooks` against a budget of 2 — #16859). ⛔ Not "only for what distance + * cannot reach": that was this line until #17361, and it sends an author + * holding a confidently wrong suggestion away from the option that fixes it. + * Plain case / underscore slips still need no entry — the fallback folds + * those already, and a second spelling of a covered probe is a dead row. */ aliases?: Readonly>; /** diff --git a/packages/spec/src/shared/suggestions.zod.ts b/packages/spec/src/shared/suggestions.zod.ts index e0dc2cf66c..2887029fe2 100644 --- a/packages/spec/src/shared/suggestions.zod.ts +++ b/packages/spec/src/shared/suggestions.zod.ts @@ -328,9 +328,15 @@ export interface StrictUnknownKeyErrorOptions { /** * Semantic near-misses: a different *word* for the same intent, usually * borrowed from a neighbouring schema or product where that word is correct. - * Edit distance cannot reach these, so they are named explicitly; plain - * case/underscore slips are left to {@link findClosestMatches}. Map keys are - * matched case-insensitively with `_` / `-` / space separators removed. + * Consulted BEFORE {@link findClosestMatches} and preferred over it, so an + * entry answers both the near-miss distance cannot reach AND the one it + * reaches and gets wrong — `hosts → network` is 2 edits from the declared + * `hooks` against a budget of 2, and the entry is what keeps the author off + * lifecycle hooks (#16859; this line asserted only the first half until + * #17361 measured 41 overruling entries across the spec). Plain + * case/underscore slips are still left to the fallback, which folds them. + * Map keys are matched case-insensitively with `_` / `-` / space separators + * removed. */ aliases?: Readonly>; /**