Skip to content

fix(spec): gate (a)'s remedy names every retirement route instead of claiming tombstoning is the only one - #12630

Merged
os-zhuang merged 2 commits into
mainfrom
claude/issue-12574-gate-a-route3-text
Aug 27, 2026
Merged

fix(spec): gate (a)'s remedy names every retirement route instead of claiming tombstoning is the only one#12630
os-zhuang merged 2 commits into
mainfrom
claude/issue-12574-gate-a-route3-text

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

Fixes#12574

Gate (a) of packages/spec/scripts/build-schemas.ts — "N authorable key(s) disappeared from the contract" — told a retiring dev to "tombstone it instead of deleting it" and closed with:

A tombstone that has aged out (~two majors) is the ONE legitimate reason to delete a line here

That is a claim of exhaustiveness, and it is false as stated. This PR replaces the remedy text. Message text only — the gate's accept/reject behavior is unchanged.

Clause-②

Clause-②: no (gate message text only; accept/reject behavior unchanged, measured).

The defect, measured on this tree

The gate's own check (c) already admits three proofs for a baseline-line deletion, each computed in-gate (#4650):

  1. an aged-out tombstone (declared in RETIRED_KEYS_BY_MAJOR);
  2. the def is not reachable from the metadata-type roots — nothing parses it (2026-08-02 ruling);
  3. the whole def stopped being emitted (adjudicated by the manifest deletion gate).

So the gate's logic has admitted routes 2 and 3 all along; only gate (a)'s text claimed tombstoning was the only route. Re-measured on today's main before writing anything:

checkresult
tombstone it instead of deleting it in build-schemas.tspresent (line 817) — phrasing still stands
the ONE legitimate reason to deletepresent (line 824) — phrasing still stands
kernel/HotReloadConfig:distributedConfig in the committed baseline0 hits — the key left by deletion, no tombstone
control kernel/HotReloadConfig:stateStrategy1 hit (authorable-surface/kernel.json:227) — control survives

The route-3 evidence in the card still reads as stated, and the dispatch ruling's ground (iii) holds: the logic admits route 3, the text does not. Not falsified.

The cost was paid by a named dev in one shift: the #12428 dev started on the route its sibling #12425 had just landed green in this same file, hit gate (a), read the remedy, and switched to tombstoning. Tombstoning was the right answer there — the key was leaving a def that survives — but the reasoning came from the message rather than from that distinction, which the message did not draw.

What the new text does

It stops decreeing and asks the precondition question instead:

Does anything still PARSE this def — can an author keep writing this key?

The wording was written against .claude/skills/spec-property-retirement/SKILL.md section 2 (the route-choice table) so the gate and the playbook agree on the preconditions.

Behaviour neutrality — measured, not asserted

  1. Mechanical: every changed line in build-schemas.ts is a template-literal fragment inside the console.error(...) argument of gate (a). Filtering the diff to lines that are not a backtick-prefixed string fragment yields nothing — no condition, no control flow, no exit code, no process.exit moved. +23 / -3 lines, all inside one string.
  2. Empirical: the gate's own check-mode battery — the 62 pre-existing tests in build-schemas-check-mode.test.ts, which exercise (a0), (a), (b), (b2) and all three of (c)'s proofs including the trip conditions — is green unchanged against the new text. Same trips, same passes. The suite now runs 63 (the 62 plus the new message pin).

Verification

All through scripts/pm/os-verify-lock.sh, exit codes captured before any pipe, and re-run after merging origin/main. Everything below is from the final commit 1c0b3e302:

checkverdict line
pnpm --filter @objectstack/spec build (runs the gen:schema gates)VERDICT command-exit 0; authorable-defaults/ verified — 1255 default(s) unchanged
pnpm --filter @objectstack/spec check:generatedAll 14 generated artifacts are up to date.
check-mode suite (the gate's own tests)Test Files 1 passed (1) · Tests 63 passed (63)
pnpm --filter @objectstack/spec testTest Files 435 passed (435) · Tests 11526 passed (11526)
pnpm --filter @objectstack/spec typecheckgreen — tsc --noEmit + check:scripts-typecheck + check:test-typecheck
pnpm check:nul-bytesOK (scanned 6979 text file(s) … no raw ASCII control bytes)
pnpm check:cross-package-test-inputsOK: 20 package(s) read outside themselves, all declared
pnpm check:engine-double-contractOK — 689 pinned, 134 in the DEBT ledger, 3 exempt
pnpm check:where-matcher303 matcher(s) discovered … none new
pnpm check:query-options-erasureratchet holds … none new
pnpm check:test-source-aliasOK — 72 packages with tests scanned
pnpm --filter @objectstack/spec check:authorable-surfaceexit 0 — 1595 schemas
node scripts/pm/bare-root-worklist.mjs --self-testOK self-test: … none stale, none missing, none contradicted
pnpm check:pm-dispatch-gatesdispatch-gates self-test: 719 cases pass

The last two are the obligations dispatch-gates.mjs names for a gate-script edit; both are green with no new ledger row, as a message-text change should be.

Typecheck really covers the diff (the NOT-MEASURED trap): tsc -p tsconfig.scripts.json --listFiles lists both edited files, so check:scripts-typecheck's green is a reading about them, not around them.

Gate list derived with node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack (2 paths, three-dot vs merge base). Left to CI: check:type-check-coverage / check:type-check-debt — the debt ratchet refuses an unbuilt workspace closure and wants the whole monorepo built, which is CI's run; the new test file sits in packages/spec/scripts, already inside tsconfig.scripts.json, so it is not a new unaccounted-for test surface. Also left to CI: the repo-wide pnpm lint sweep and the remaining path-derived families in the table above that CI runs on every PR regardless.

Reverse verification

With the text change reverted (git checkout of the base revision's build-schemas.ts) and the updated test kept, the new pin turns RED — the predicted direction:

MUTATION: 'ONE legitimate reason' occurrences = 1 (expect 1)
MUTATION: 'Does anything still PARSE this def' occurrences = 0 (expect 0)
MUTATION CONFIRMED ON DISK
Tests 1 failed | 62 skipped (63)
-> expect(output).toContain('Does anything still PARSE this def')

Restore proven on disk rather than by exit code: git checkout HEAD -- path (naming HEAD, since the mutation leg also wrote the index), then git hash-object equal to the HEAD blob and git diff HEAD empty. The script carried a trap ... EXIT INT TERM restore with absolute paths throughout. No rebuild is involved in this ablation and none is owed: the check-mode test copies scripts/ into a temp sandbox and runs the script with tsx, so it resolves from source, never from dist/.

Changeset

None — following the precedent for build-script message-text changes. The nearest precedent is 82bfb6ec1 (#6309, "锚点漂移提示按实测方向措辞"), which reworded a build-schemas.ts gate message and updated the same check-mode test — exactly this PR's two-file shape — and carried no changeset. By contrast 25982166e (#5898) did carry one, because it changed the tombstone aging logic. The distinction is behaviour, and this PR has none: packages/spec/scripts/ is not published, and nothing user-visible ships. skip-changeset applied accordingly.

Scope

Two files, both named in the dispatch. Horn 1 only. The horn-2 question (whether route 3 is legitimate at all, and the disposition of the already-deleted distributedConfig) is untouched here — the fork condition did not fire, so it was never in scope. No out-of-scope findings.


Generated by Claude Code

…claiming tombstoning is the only one (#12574)
Gate (a) in build-schemas.ts told a retiring dev to "tombstone it instead of
deleting it" and that an aged-out tombstone was "the ONE legitimate reason to
delete a line here". That is a claim of exhaustiveness, and the gate's own
check (c) already contradicts it: a baseline-line deletion is legitimate on any
of three computed proofs -- aged-out tombstone, a def unreachable from the
metadata-type roots, or a whole def leaving the build (#4650).
The cost was measured on a named dev: #12428 started on the unreachable-def
route its sibling #12425 had just landed green in this same file, hit gate (a),
read the remedy, and switched to tombstoning. Tombstoning was right for #12428
-- the key was leaving a def that survives -- but the reasoning came from the
message rather than from that distinction, which the message did not draw.
Message text only; the gate's accept/reject behavior is unchanged. The remedy
now asks the precondition question ("does anything still PARSE this def?")
and gives both answers, carries the 2026-08-02 ruling's limit on the
unreachable-def waiver, and keeps the aged-out tombstone as a route rather than
as the reason. Pinned by a new check-mode test asserting the fork, both routes,
and the absence of the exhaustiveness claim.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012xGvxcwPRTJfA7RfjXEYA4
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

Nothing 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
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

Coarse fallback — 0 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 0043c92240ffe5b53df43753f9ab1edd2446b474packageMentionDocs.

@os-zhuangos-zhuang added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Aug 27, 2026 — with Claude
@os-zhuang
os-zhuang marked this pull request as ready for review August 27, 2026 02:16
@os-zhuang
os-zhuang added this pull request to the merge queueAug 27, 2026
Merged via the queue into main with commit 9c32357Aug 27, 2026
38 checks passed
@os-zhuang
os-zhuang deleted the claude/issue-12574-gate-a-route3-text branch August 27, 2026 02:38
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/sskip-changesetPR has no user-facing published change; bypasses the changeset gateteststooling

Projects

None yet

2 participants

@os-zhuang@claude