Repository navigation
fix(spec): judge a nested RETIRED_KEYS_BY_MAJOR row instead of ignoring it - #18618
Conversation
…s judged, not ignored `currentKeys` is built from `schema.properties` one level deep, so a dotted `RETIRED_KEYS_BY_MAJOR` row matched nothing in the map check (b2) consults and was silently ignored rather than judged: a fabricated nested row passed `check:authorable-surface` at exit 0 with zero failures, against a lit control (a live top-level key) refused at exit 1. Resolve a registered nested row against the schemas this build emits, for the registered rows only. `currentKeys` and `currentEntries` are untouched, so checks (a0)/(a)/(b) and the published authorable-surface baseline are byte-identical; recursing the walk in place measures at +14,376 lines across all 14 shards. (b2) now refuses a nested row whose path resolves LIVE, on the same message as a top-level one. (b3) refuses a nested row whose def is emitted but whose path is not: a nested key never reaches authorable-surface/, so no aging clock could have let it go and the row is simply wrong. A row whose whole def is gone stays unjudged — that is the RETIRED_DEFS_BY_MAJOR steady state. Claude-Session: https://claude.ai/code/session_01JbZnqu8bt6YqfJsr9vaFb3 Co-authored-by: Claude <noreply@anthropic.com>
The shipped tree exercises three of the resolver's traversals and none of its refusals, so a rule that stopped following `$ref`, or started descending into a record's value shape, would move nothing a sandbox case can see until the first row lands on that shape — as a false red or a false green. Found by the first run of these pins: 36 of the emitted refs are spelled `#` (a recursive schema pointing at its own document), which the first cut read as external and would have reported as a typo'd path. Claude-Session: https://claude.ai/code/session_01JbZnqu8bt6YqfJsr9vaFb3 Co-authored-by: Claude <noreply@anthropic.com>
…controls Four acceptance cases against the real generator in a sandbox: the card's fabricated nested row is refused, a live top-level key is still refused on the same message, a live NESTED key is now refused by (b2) too, and two dark controls pass — a real nested retirement, and a fabricated row under a def this build does not emit. Without the second pair, a check that refused every dotted row would satisfy the probe and the lit control alike. Found by the first run of these pins: the baseline carries 4 dotted keys that are TOP-LEVEL property names carrying a dot (`@odata.context` and siblings, a SCIM extension URN), so routing asks the emitted schema before the spelling — pinned by a fifth case and by an enumerated roster that a widened surface would blow up. Claude-Session: https://claude.ai/code/session_01JbZnqu8bt6YqfJsr9vaFb3 Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckNothing in this diff resolved to a documentable surface (no symbol, route or SDK anchor derived from 0 changed package(s)), so this run has no opinion about the docs. What this run could not see
Coarse fallback — 0 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): |
Contract reviewServed-tier: 88/88 Isolated at-tier review. Fed: card #17969 (body and comments) and PR #18618 (body, diff, file list, check runs). Withheld by design: the dispatch order and the dispatching seat's conclusions; no seat comment was treated as a finding of fact. Every reading below was taken on a detached worktree at the head sha above (base ① Derived judgments
② Semver level
③ Boundary flags
Implemented-by: VERDICT: PASS Generated by Claude Code |
Clause-② adopted — PASS,双载体同笔剥;
|
| 旗 | 处置 |
|---|---|
| 1 · 登记表 docblock 的 Lifecycle 段少描述了一个状态,且 (b3) 的「删条目」处方与「Entries are permanent」相抵 | 待立卡,见下 |
2 · check:cross-package-test-inputs 依本地有无 packages/spec/dist 给相反判决(复核员独立复现过) |
⛔ 不立孪生 —— 已有 #18353 / #18440,本席逐张读过标题确认 |
| 3 · def 未 emit 的行不判 | 既有语义、按设计,无动作 |
4 · 新 — 祖先被墓碑化/走 guidance 路移除/经 $ref 指向已墓碑 $def 时,(b3) 可能拒绝一条真实的行,而印出的处方对这一形状是错的 |
待立卡,见下 |
5 · 新 — resolver 走 items 但不走 prefixItems |
并入 4 的卡 |
| 6 · CI | 所有 required context 均 success,0 失败;未碰受管面 |
nested-authorable-keys.ts 里 prefixItems 计数读 0(与复核员一致);但本席找「有多少 emitted schema 带 tuple」时搜错了树(搜了 packages/spec/src,而 emitted schema 在生成物里),所以那一半是 NOT MEASURED,⛔ 不是反证。(b3) 的处方原文与登记表 docblock 的两句,本席的 grep 窗口也没取到。⇒ 立卡时会写明哪一部分是复核记录的、哪一部分是本席自己测到的,⛔ 不把转述写成第一手。
随后按 landing-operations.md 走落地前检 → 转 ready → 挂 auto-merge。
Generated by Claude Code
Fixes #17969
Clause-②: yes
What was broken, and the reading that says so
check:authorable-surfacebuildscurrentKeysfromschema.propertiesone level deep(
packages/spec/scripts/build-schemas.ts). Check (b2) — the guard that refuses aRETIRED_KEYS_BY_MAJORentry naming a key the build still emits as LIVE — consults that map, so adotted row matched nothing in it and was silently ignored rather than judged. 48 of the shipped
rows are dotted.
Reproduced on this branch before the fix, by ablation, with the card's own controls:
check:authorable-surfacesystem/SchemaLevelIsolationStrategy:performance.zzNotARealKey9999system/SchemaLevelIsolationStrategy:strategy, a live top-level keyThe coupling, measured before the fix was written
Three lines below
currentKeysthe same map iscurrentEntries, the publishedauthorable-surface baseline. The dispatch asked for a measurement rather than an argument, so:
currentKeysin place moveauthorable-surface/**? Measured, not reasoned:the one-level loop was replaced with a recursive walk,
gen:schemawas run, and the diff read+14,376 lines across all 14 shards (
ai 320, api 6099, automation 496, data 1393, identity 83, integration 170, kernel 1997, marketplace 86, qa 93, security 90, shared 6, studio 187, system 1648, ui 1709). The script was then restored byte-identical(
git hash-objectback tob4f0ac58c00086a60528b88acdf1c45f13115387,git diff HEADempty).So yes: widening in place publishes the whole nested key space as a ratcheted, author-facing
contract.
currentKeysandcurrentEntriesare untouched; the nested state is resolved for theregistered rows only and reaches nothing that emits. Deliberately no nested map is
materialised — the hazard here is a nested key space lying around for a later author to pipe
into
currentEntries, so there is nothing to pipe.git diff --exit-code -- packages/spec/authorable-surface/is clean after a full
gen:schemaon this branch, andcheck:generatedreports all 15 generatedartifacts up to date.
What the change does to each check that reads
currentKeyscurrentKeys, which is byte-for-byte whatit was.
reporting every nested key as "vanished" against a baseline that has never recorded one.
TOP-LEVEL transition, so no nested retirement that owes nothing today starts owing one. This is
the behaviour change the card itself flagged under its shape 1, and it is not made here.
the same expression and the same message as before; a nested row is judged on the resolved state
of its path.
state is the aged-out steady state (b2) exempts: the tombstone carried
[RETIRED], aged twomajors, and check (c) let its baseline line go, with evidence at every step. A nested key reaches
none of that — it has no baseline line, no mark and no aging clock — so no gated route can produce
a registered nested row whose path is absent, and the one origin left is that the row is wrong.
RETIRED_DEFS_BY_MAJORsteady state, adjudicated by thejson-schema.manifest/ratchet. Threeof the 48 nested rows are in it (the change-management family, retired whole).
Population, which the card recorded as unmeasured
Censused over the 48 dotted rows the shipped table declares: 45 resolve to a tombstone, 3 name a
def this build does not emit, 0 resolve LIVE, 0 fail to resolve. The gate is green on the shipped
table, and the sixth test case below keeps that measured rather than assumed.
Acceptance — a probe, a lit control, and two dark controls
Pinned end to end in
scripts/build-schemas-check-mode.test.ts, spawned against the real generatorin a sandbox that substitutes its own
RETIRED_KEYS_BY_MAJOR:Reverse verification. With the fix committed,
packages/spec/scripts/build-schemas.tswasrestored to its pre-fix bytes (
git restore --source=BASE, on-disk hash verified equal to the baseblob,
nestedStatesoccurrences 3 to 0) and the block re-run: exactly 2 of 7 cases go red — thePROBE and the live-NESTED case, both on
expected +0 to be 1, i.e. the gate exiting 0. The fivecontrols stay green. Restored afterwards with
git checkout HEAD --;git diff HEADempty and theon-disk blob back to
4948f991ee792944f90e91747054950bf7726fe7.A discovery from the first run of the new pins, recorded because it moved the design: the
published baseline carries 4 dotted keys that are TOP-LEVEL property names containing a dot —
api/ODataResponse:@odata.contextand two siblings, and a SCIM extension URN onidentity/SCIMUser. So "has a dot" is not the same question as "names a path": routing asks theemitted schema first and reads a row as a path only when this build emits no top-level property of
that exact name. A fifth test case and an enumerated roster pin it, and the roster is also what a
widened published surface would blow up. A second discovery, from the resolver's own unit pins:
36 of the emitted refs are spelled
#(a recursive schema pointing at its own document), whichthe first cut read as external and would have reported as a typo'd path.
Evidence
ESLint, narrowed and declared:
pnpm exec eslint --no-inline-config --format jsonover the 4 filesthis branch touches — 4 files linted, 0 errors, 0 warnings, file count read from the JSON
report. The universe eslint itself configures is
**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}minusNEVER_LINTED, read fromeslint.config.mjs; and the narrowing is sound because that confignever enables type-aware linting for any file (no
parserOptions.project, no typed@typescript-eslintrules — the config says so in its own measured note), so this diff cannot movethe verdict on a file it does not touch. The repo-wide sweep is CI's.
Gate families, derived from the real change set rather than from the dispatch list
(
node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack): 59 derived, 28 run herewith recorded exit codes, 1 NOT MEASURED (
check:type-check-debtexits 3 — it refuses without awhole-repo build closure, which is CI's), 1 not measured by a runner cap
(
check:pm-dispatch-gates, killed at 420s by this seat's owntimeout), 30 declared to CI —cross-cutting families this 4-file spec-scripts diff cannot move.
File surface
packages/spec/scripts/build-schemas.ts— the routing, (b2)'s nested limb, (b3), andisRetireddelegating to one definition of the tombstone predicate.
packages/spec/scripts/lib/nested-authorable-keys.ts— new. The resolution rules, and theauthority on what they cannot see.
packages/spec/scripts/nested-authorable-keys.test.ts— new. 17 unit pins on the resolver.packages/spec/scripts/build-schemas-check-mode.test.ts— the 7 acceptance cases above.Untouched, as declared at claim time:
packages/spec/authorable-surface/**(byte-identical),packages/spec/scripts/lib/renamed-defs.ts, and theRETIRED_KEYS_BY_MAJORregistry — the twoablations that wrote to the tree were restored byte-identical and proved so.
No changeset,
skip-changeset. Measured rather than assumed:@objectstack/specshipsfiles[]= dist, json-schema, liveness, prompts, llms.txt, README.md,src/**/*.zod.ts,CHANGELOG.md, api-surface, spec-changes.json —
scripts/is not in it, and neitherbinnorexportsreaches it. Grepping the built published paths for every symbol this diff adds(
nestedAuthorableKeyState,isNestedAuthorableKey,isRetiredJsonSchemaNode,unresolvableNested,nested-authorable-keys) returns 0 files each, against positive controlsthat return 8 (
SchemaLevelIsolationStrategySchema) and 25 (RETIRED_KEYS_BY_MAJOR).On the
Clause-②: yesdeclarationDeclared
yesconservatively at claim time, because the honest answer depended on the measurementabove. The delivered diff leaves the published authorable surface byte-identical and only gives
a checker a discrimination it lacked over a class of inputs; nothing an author may write changes,
and nothing published moves. That is the evidence, stated plainly — the contract review decides the
declaration, and this PR does not lower it.
Acceptance notes
RETIRED_KEYS_BY_MAJORdocblock now under-describes its own gate by one state. Its"Lifecycle" paragraph says "The one state the gate rejects is an entry naming a key that is still
LIVE", and after this change a nested row has a second rejected state. The registry was declared
read-only for this card, so it is not edited here: a one-paragraph follow-up, noted rather than
filed.
check:cross-package-test-inputsreds in a worktree that has a builtpackages/spec/dist/.Measured here with a control: this branch reds it (naming
packages/cli/test/init-created-files-summary.e2e.test.tswalking
packages/spec/dist/, a radius no declared glob reaches), while the same gate on acheckout with no
packages/spec/dist/exits 0. Independent of this diff, which touches neitherpackages/cli/**nor any declaration the gate reads. Two readings and both are worth a card: agate whose verdict depends on local build state, or a true finding CI cannot see because it runs
the gate without that build. Reported for filing rather than filed.
e81c4e5f30andmainhas moved since; the merge and CI convergence arethe seat's, per the dispatch contract.
Generated by Claude Code