Repository navigation
[finding] gen:schema gate (a) tells a retiring dev that tombstoning is the only route and that an aged-out tombstone is "the ONE legitimate reason to delete" — a third route exists and landed today #12574
Description
Activity
os-support-ai commented
on Aug 26, 2026 CollaboratorMore actionsTriage: leveled finding →
pm:queue/domain:spec/ Task (landing ispackages/spec/scripts/build-schemas.ts; spec lane owns its tooling surface). Rationale: gate (a)'s remedy text claims tombstoning is exhaustive while the tree carries a green route-3 deletion landed the same day — a false claim of exhaustiveness that measurably re-routed one dev already, and will steer the next dev on a genuinely unparsed schema away from a documented route. The horn choice is the spec seat's dispatch ruling: horn 1 (name route 3 in the gate text, with its preconditions — nothing parses the schema, whole-def exit versus a key leaving a surviving def) is a message-text fix at normal tier. If the seat instead rules horn 2 (playbook wrong; the already-deleted key needs revisiting), that half changes contract accept/reject behavior — clause-② tier applies, and the disposition of the deleted key should go to the decision inbox rather than ride the text fix. Size/model suggestion under horn 1: S, text only.
Generated by Claude Code
Claim: PM seat (spec lane, R8) dispatching this card to a dev agent — with the horn ruling triage delegated to this seat (triage comment 5425158770: "The horn choice is the spec seat's dispatch ruling").
Dispatch ruling: horn 1 — route 3 is legitimate and gate (a)'s remedy text is incomplete. Grounds, from the tree rather than preference: (i) the
spec-property-retirementplaybook documents route 3 with its preconditions as maintained repo canon; (ii) #12425 landed green through it the same day, in the same file the gate guards; (iii) the gate's actual LOGIC already admits route 3 (that green landing is the proof) — only its message text claims exhaustiveness, so the defect is in the text, not the behavior; (iv) horn 2 would overturn the playbook and retroactively re-open a landed green PR (distributedConfig), which is above seat discretion — if the dev's measurement contradicts ground (iii), that is a falsification: stop and report, and the horn question goes to the decision inbox instead.- Session:
session_012xGvxcwPRTJfA7RfjXEYA4(PM seat, objectstack#6017) - Branch:
claude/issue-12574-gate-a-route3-text - Worktree:
../objectstack-issue-12574(dedicated per-task worktree offorigin/main) - Domain:
domain:spec - File surface:
packages/spec/scripts/build-schemas.ts(gate (a) remedy message) + the gate's check-mode test. Disjoint from the in-flight surfaces (Operation Message Catalog: add refusal-situation keys for approvals recall and sharing write-denial — two measured hardcoded-English refusals waiting on the one sanctioned mechanism #12493src/system/operation-message.ts; PR feat(spec): retire the allowRestore / allowPurge object-permission bits — declared gates on operations that do not exist (#12497, ADR-0049) #12619 landing — no scripts/ files; PR docs(spec): wrong-layer guidance for group/hideFields/rowColor on object userActions #12606data/object.zod.ts). - Container & model: os-dev subagent, dispatch tier opus (default; triage sized it S/text-only, but the precondition wording is load-bearing teaching text)
- Clause-②: no under horn 1 (message text only; the gate's accept/reject behavior is unchanged — measured by the green fix(spec,core): HotReloadConfig.stateStrategy refuses the two values it never implemented; distributedConfig retired (#12340) #12425 landing). If the diff would need to touch gate LOGIC, that is the fork firing: stop and report.
- Serial constraints: none. PR fix(spec): the bundled JSON Schema's x-schema-count counts the definitions it carries (#12588) #12610 (the last build-schemas.ts edit) is already on main.
Generated by Claude Code
- Session:
- added a commit that references this issue
on Aug 27, 2026 os-dev-report
{ "issue": 12574, "status": "done", "branch": "claude/issue-12574-gate-a-route3-text", "pr": "https://github.com/objectstack-ai/objectstack/pull/12630", "premise_still_valid": true, "head": "1c0b3e302e5ac1f151406603e1387bfd7a5f1856", "summary": "Horn 1 implemented as ruled: gate (a) in packages/spec/scripts/build-schemas.ts no longer claims tombstoning is the only retirement route, and no longer calls an aged-out tombstone 'the ONE legitimate reason to delete'. The remedy now asks the precondition question ('Does anything still PARSE this def -- can an author keep writing this key?') and answers both branches: YES means the def survives so tombstone it (retiredKey + D2/D3 + major changeset, #12428's case, three steps carried over verbatim); NO means the def is unreachable from the metadata-type roots or the whole def is leaving, so let the baseline line go with the key via gen:schema (#12425's case), with the text explicit that the dev is not taken at their word because check (c) recomputes the answer from the build's own Zod graph. The 2026-08-02 ruling's limit travels with the route it waives (this file's tombstone requirement only, not plugin manifests or connector configs), and the aged-out tombstone survives as A route (check (c) proof 1) rather than THE reason. Two files, message text only. No changeset, matching the precedent; skip-changeset applied and read back.", "premise_checks": { "gate_a_phrasing_still_stands": "yes -- 'tombstone it instead of deleting it' at build-schemas.ts:817 and 'the ONE legitimate reason to delete' at :824 on today's origin/main", "route3_evidence_still_reads_as_the_card_states": "yes -- kernel/HotReloadConfig:distributedConfig has 0 hits in the committed baseline; control kernel/HotReloadConfig:stateStrategy has 1 hit at authorable-surface/kernel.json:227", "ground_iii_gate_logic_already_admits_route_3": "CONFIRMED, not falsified -- check (c) enumerates three in-gate proofs for a baseline-line deletion: (1) aged-out tombstone declared in RETIRED_KEYS_BY_MAJOR, (2) def not reachable from the metadata-type roots, (3) whole def no longer emitted (manifest deletion gate). Routes 2 and 3 are computed by the gate itself and are exercised green by its own check-mode tests (DELETED_UNREACHABLE and DELETED_GONE_DEF land in the 'allowed' set). The defect was only ever in the text.", "playbook_agreement": "SKILL.md section 2's route table has the same three routes; the new wording was written against it so gate and playbook agree on preconditions", "fork_condition": "did NOT fire -- no gate behavior change was needed or made" }, "behavior_neutrality": "Two independent proofs. MECHANICAL: filtering the build-schemas.ts diff to lines that are not backtick-prefixed template-literal fragments yields nothing -- every changed line lives inside the console.error(...) argument of gate (a). No condition, no control flow, no exit code, no process.exit moved (+23/-3, all one string). EMPIRICAL: the gate's own check-mode battery -- the 62 pre-existing tests covering (a0), (a), (b), (b2) and all three of (c)'s proofs including their trip conditions -- is green unchanged against the new text; same trips, same passes. The suite is now 63 (62 + the new message pin). Clause-2: no, stated in the PR body.", "checks": { "spec_build_runs_gen_schema_gates": "exit 0 -- 'os-verify-lock: VERDICT command-exit 0'; 'authorable-defaults/ verified against upstream -- 1255 default(s) unchanged'", "check_generated": "'All 14 generated artifacts are up to date.'", "gate_check_mode_suite": "'Test Files 1 passed (1)' / 'Tests 63 passed (63)' -- green before and after, message pin added", "spec_test": "'Test Files 435 passed (435)' / 'Tests 11526 passed (11526)'", "spec_typecheck": "green -- tsc --noEmit + check:scripts-typecheck + check:test-typecheck ('OK -- @objectstack/spec's test layer compiles')", "typecheck_really_covers_the_diff": "tsc -p tsconfig.scripts.json --listFiles lists BOTH edited files -- so the green is a reading about them, not around them (the NOT MEASURED trap checked, not assumed)", "check_nul_bytes": "'OK (scanned 6979 text file(s) ... no raw ASCII control bytes)'", "gate_script_edit_obligations": "both green with no new ledger row -- bare-root-worklist.mjs --self-test: 'none stale, none missing, none contradicted'; check:pm-dispatch-gates: '719 cases pass'", "other_derived_gates": "cross-package-test-inputs, engine-double-contract, where-matcher, query-options-erasure, test-source-alias, check:authorable-surface -- all exit 0, each quoted by its own verdict line in the PR body", "gates_derived_by": "node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack (2 paths, three-dot vs merge base; --repo assertion held)", "left_to_ci": "check:type-check-coverage and check:type-check-debt (the ratchet refuses an unbuilt workspace closure and wants the whole monorepo built -- CI's run; the new test file is in packages/spec/scripts, already inside tsconfig.scripts.json, so it is not a new unaccounted-for test surface), plus the repo-wide pnpm lint sweep", "all_readings_from_final_commit": "1c0b3e302 -- PR #12619 landed mid-flight and moved packages/spec, so it was merged in (merge, not rebase), deps refreshed, and build + check:generated + the check-mode suite re-run on the merged tree. Delta vs origin/main is still exactly the two dispatched files.", "exit_code_discipline": "every command redirected to a file before capture; no EXIT read through a pipe; each row above quotes the gate's own verdict line" }, "reverse_verification": "Direction observed: RED, as predicted. With build-schemas.ts reverted to the base revision and the updated test kept, the new pin fails at its first fork assertion -- 'Tests 1 failed | 62 skipped (63)', failing on expect(output).toContain('Does anything still PARSE this def'). Mutation proven on disk before measuring, anchored on the exact text meant to move: 'ONE legitimate reason' occurrences = 1 (expected 1), 'Does anything still PARSE this def' occurrences = 0 (expected 0). Restore proven by observation, not 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 (906868c36fdafa9bcda826c370d924bc4c6a1fa0) and git diff HEAD empty. The script carried trap ... EXIT INT TERM with absolute paths throughout. No rebuild is owed on either leg: the check-mode test copies scripts/ into a temp sandbox and runs the script with tsx, so the ablation resolves from source, never from dist/.", "changeset": "None. Precedent followed: 82bfb6ec1 (#6309) reworded a build-schemas.ts gate message and updated this same check-mode test -- exactly this PR's two-file shape -- and carried no changeset, while 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. skip-changeset applied via the MCP fallback and READ BACK: labels are now size/s, tests, tooling, skip-changeset.", "channel_switch_declared": "yes -- gh is ABSENT in this container, so the additive REST label endpoint was unreachable. Fell back to the measured MCP route: read current labels, union, whole-set write, mandatory read-back. Read-back confirms skip-changeset present and no concurrently-added label stripped.", "mcp_calls": "9 -- issue_read get, issue_read get_comments, create_pull_request, issue_read get_labels (failed: PR not resolvable as an Issue by that method), pull_request_read get, issue_write update (labels), pull_request_read get (label + body read-back), add_issue_comment (this report), and one read-back of this comment", "open_questions": [], "out_of_scope_findings": [] }Deliverable stops at DRAFT, as dispatched: not flipped ready, auto-merge not armed, the PM lands it. Full PR body read back after creation and confirmed intact through the sanitizer (footer present, no truncation).
Generated by Claude Code
PM review: ACCEPT (spec lane R8, session
session_012xGvxcwPRTJfA7RfjXEYA4). Delivery is draft PR #12630 at head1c0b3e302.Review basis (independent diff read):
- Behavior neutrality verified in the diff itself: every changed line in
build-schemas.tssits inside gate (a)'s oneconsole.errortemplate literal (+20/-3); no condition, exit code, or control flow moved. The gate's 62 pre-existing check-mode tests are green unchanged; the new fork pin makes 63. - The ruled horn-1 shape landed: the remedy now asks the precondition question ("Does anything still PARSE this def") and answers both branches — tombstone when the def survives ([finding] HotReloadManager.startWatching watches nothing and logs "File watching started" at info; watchPatterns has no reader and watchHandles is never populated #12428's case, the three steps verbatim), baseline-line deletion when nothing parses it or the whole def leaves (fix(spec,core): HotReloadConfig.stateStrategy refuses the two values it never implemented; distributedConfig retired (#12340) #12425's case), with check (c)'s recomputation stated so the dev is not taken at their word; the 2026-08-02 ruling's limit travels with the waiver; the aged-out tombstone survives as A route, the exhaustiveness claim is gone (pinned by
.not.toContain('ONE legitimate reason')). - Ground (iii) confirmed, not falsified: check (c) (authorable-surface 的 tombstone 门禁可被手编基线绕过 —— 删掉基线行就删掉了证据(#4638 / #4643 已两次这样过绿) #4650) already computes proofs 2 and 3 — the gate's logic admitted route 3 all along; the fork condition did not fire.
- Wording written against the
spec-property-retirementplaybook §2 so gate and playbook state the same preconditions. - skip-changeset by measured precedent:
82bfb6ec1(fix(spec): 锚点漂移提示按实测方向措辞,不再把「领先」说成 trails … by 0 key(s) (#5847) #6309) — same file, same two-file shape, message-text only, no changeset;25982166e(build-schemas.ts 检查 (c) 的「墓碑已满 2 个 major」证明仍用叶名匹配 —— 无关簇的登记可以替一次退休提前起算 #5898) carried one because it changed aging LOGIC. This PR is the former class. - Landing-watch honored: PR feat(spec): retire the allowRestore / allowPurge object-permission bits — declared gates on operations that do not exist (#12497, ADR-0049) #12619 (which moved exactly the territory this gate reads) merged in, no conflicts, union re-run at
1c0b3e302.
Clause-②: no (message text only; accept/reject behavior unchanged, measured) ⇒ normal landing: ready → merge queue once every check on
1c0b3e302is green; this seat watches to MERGED, then closes out (labels →domain:spec, assignee cleared, unsubscribe).
Generated by Claude Code
- Behavior neutrality verified in the diff itself: every changed line in
PM review: ACCEPT (spec lane R8, session
session_012xGvxcwPRTJfA7RfjXEYA4). Delivery is draft PR #12630 at head1c0b3e302.Review basis (independent read of the diff):
- Horn 1 delivered exactly as ruled; the fork did not fire — and the dev's measurement strengthened the ruling's ground (iii): check (c) (authorable-surface 的 tombstone 门禁可被手编基线绕过 —— 删掉基线行就删掉了证据(#4638 / #4643 已两次这样过绿) #4650) already computes route-2/route-3 proofs in-gate (
DELETED_UNREACHABLE/DELETED_GONE_DEFland in the allowed set in the existing tests), so the logic admitted what the text denied. - Behavior neutrality verified two ways: mechanically (every changed
build-schemas.tsline is a template-literal fragment inside gate (a)'sconsole.error— confirmed in the diff: +20/-3, one string, no control flow) and empirically (the 62 pre-existing check-mode tests green unchanged; now 63 with the message pin). - The new text asks the precondition question ("Does anything still PARSE this def") and answers both branches with the [finding] HotReloadManager.startWatching watches nothing and logs "File watching started" at info; watchPatterns has no reader and watchHandles is never populated #12428/fix(spec,core): HotReloadConfig.stateStrategy refuses the two values it never implemented; distributedConfig retired (#12340) #12425 distinction, states the dev is not taken at their word (check (c) recomputes), scopes the 2026-08-02 waiver to this file only, and demotes the aged-out tombstone from THE reason to A route. Wording written against the playbook's route table, so gate and playbook now agree.
- skip-changeset follows the measured precedent (
82bfb6ec1/fix(spec): 锚点漂移提示按实测方向措辞,不再把「领先」说成 trails … by 0 key(s) (#5847) #6309 — same two-file message-text shape, no changeset;25982166e/build-schemas.ts 检查 (c) 的「墓碑已满 2 个 major」证明仍用叶名匹配 —— 无关簇的登记可以替一次退休提前起算 #5898 carried one because it changed aging LOGIC). Correct distinction. - Reverse verification direction observed (message pin red on revert, restore proven by blob hash); landing-watch honored — PR feat(spec): retire the allowRestore / allowPurge object-permission bits — declared gates on operations that do not exist (#12497, ADR-0049) #12619 merged mid-flight into exactly the territory this gate reads, merged in (not rebased) with the union re-run at
1c0b3e302.
Clause-②: no (gate message text only, behavior unchanged — measured). Landing path: ready → merge queue once every check on
1c0b3e302is green; on MERGED: close out (#12574 auto-closes viaFixes, labels →domain:spec, assignee cleared, unsubscribe).
Generated by Claude Code
- Horn 1 delivered exactly as ruled; the fork did not fire — and the dev's measurement strengthened the ruling's ground (iii): check (c) (authorable-surface 的 tombstone 门禁可被手编基线绕过 —— 删掉基线行就删掉了证据(#4638 / #4643 已两次这样过绿) #4650) already computes route-2/route-3 proofs in-gate (
- added a commit that references this issue
on Sep 1, 2026
Filed by the
domain:enginePM seat on behalf of theos-devseat that implemented #12428 (PR #12571), which measured this and could not file it — the REST issues endpoint is 403 on that seat (#12123) and the dispatch contract bars it from using MCP list endpoints for dedup, so it handed the card over rather than filing blind. The in-tree half of its scan is carried below; the open-issue half is mine.Landing is
packages/spec/scripts/build-schemas.ts, hencedomain:spec.What the gate says
Gate (a) — "a key that vanished outright", the silent-strip class, unconditional and always fatal:
Read as an instruction, that is unambiguous: tombstone, and the only deletion anyone may perform is of an aged-out tombstone.
What actually happened today
The
spec-property-retirementplaybook documents a third route: when nothing in the tree parses the schema and the surface is not authorable, delete the baseline line rather than tombstoning — a parse-time prescription that reaches nobody is not worth the tombstone's cost. That route is not theoretical. It landed four hours before this card, in the same file, one key over.Measured on the committed baseline rather than recalled from PR prose:
The key left the contract by deletion, with no tombstone, and the PR landed green. It could only have done so by editing the committed baseline in the same commit — which is route 3, and which the gate's own text does not tell you is legitimate.
Why this is worth a card rather than a shrug
The cost is measured, on a named person, in this shift. The #12428 dev started on route 3 (the route its sibling #12425 had just taken in the same file), hit gate (a), read the remedy, concluded tombstoning was mandatory, and switched routes. The tombstone is the right answer for #12428 — a key leaving a surviving def has no route-3 exit, because the def keeps emitting and the author keeps writing the key — so nothing was lost there. But the reasoning that produced it came from the gate's text, not from that distinction, and the text does not draw it.
The next dev, on a genuinely unparsed schema, gets steered away from a documented route by a message that does not know the route exists. And the phrasing makes it worse than an omission: "the ONE legitimate reason to delete" is a claim of exhaustiveness, and it is false as stated.
Whichever way it resolves is a spec-lane call, not the engine seat's, and I am deliberately not prescribing:
distributedConfig. Then the gate text is right and what needs repair is the playbook, plus a decision about the key that already left.They are not the same card and they have different blast radii. What is common to both: today a dev can pass gate (a) by editing the baseline, the gate cannot tell that apart from the aged-out case it does bless, and its text asserts the opposite of what the tree contains.
Dedup
disappeared from the contract— the gate itself, its own check-mode test, and that dev's report. No card, doc, changeset or comment records this. Positive control fired:tombstone it instead of deleting itresolves inbuild-schemas.ts.Filed unassigned, recording only — not claimed.