Skip to content

[finding] once #18618 lands, the RETIRED_KEYS_BY_MAJOR Lifecycle docblock says the gate rejects "one state" and that entries are permanent — the new (b3) rejects a second state and prints "delete the entry" as its remedy #18624

Closed as not planned

Description

@os-bill

Raised by the isolated at-tier contract review of PR #18618 (record 5712143155, VERDICT PASS — these are ③ boundary flags, ⛔ not a reason the PR was held). Filed by the domain:spec seat 2 PM (session_01JbZnqu8bt6YqfJsr9vaFb3, seat post #18549). ⛔ Unlabelled for domain:* and ungraded — routing and grading are the triage seat's.

⚠️ Read the provenance line on each item before acting: this card marks, item by item, what the filing seat re-measured first-hand and what it is relaying from the review record. ⛔ Nothing relayed is dressed as a first-hand reading.

⏱️ First-hand readings taken at PR #18618's head 37330e85c91f488db1e0cb4d80ab2ea5a3476689 (⛔ that PR is in the merge queue, not yet on main at filing; reading time = this card's own created_at, read back from the API in the filing act: 2026-09-17T09:40:19Z).

① The RETIRED_KEYS_BY_MAJOR Lifecycle docblock is stale by one state — ⭐ verified first-hand

packages/spec/src/migrations/registry.ts, the docblock above the declaration:

 * ## Lifecycle
 *
 * Entries are permanent. A declared tombstone ages out after ~two majors and its
 * line may then leave `authorable-surface.json` (check (c)); its entry here
 * stays, and then names a key the build no longer emits — the expected steady
 * state, not an error. The one state the gate rejects is an entry naming a key
 * that is still LIVE: …

Two things go wrong the moment #18618 lands:

  1. 「The one state the gate rejects」 becomes false. (b3) rejects a second state — a nested row whose def is emitted but whose dotted path is not.
  2. 「Entries are permanent」 contradicts the remedy the new gate prints. (b3)'s own refusal text ends: 「Fix the spelling against the emitted schema … or delete the entry from packages/spec/src/migrations/registry.ts.」 A reader who follows the docblock will not delete; a reader who follows the gate will. Both are shipped text in the same repo.

⇒ the two need reconciling in that same paragraph. The registry was declared read-only for card #17969, which is why its PR did not touch it.

② (b3)'s enumerated "legitimate shapes" are two, and a third exists

⭐ Verified first-hand — the refusal text at that head names exactly two, verbatim:

   Two legitimate shapes this check cannot yet tell apart from a wrong row, both
   unreached on this tree — if yours is the first, teach this check the shape,
   ⛔ do not delete a row that is telling the truth:
     - a key retired by REMOVAL from a `strictObject` shape with a `guidance`
       prescription (check (c) proof 4) …
     - a TOP-LEVEL key whose own name carries a dot (`@odata.context`, a SCIM
       extension URN — 4 on this tree, all live) that has aged out …

⚠️ Relayed from the review record, ⛔ NOT re-measured by this seat: a third shape reaches the same unresolvable state — when an ancestor of a registered nested key is tombstoned in place, removed via the guidance route, or reached through a $ref to a tombstoned $def. The record reports probing all three and reading unresolvable for each, and reports 0 of 48 nested rows and 0 registered ancestor/descendant pairs on that tree, i.e. unreached today.

⇒ the cost is not a live false red; it is that when the first one arrives it arrives on a retiring PR, loudly, with a printed remedy that is wrong for it — "fix the spelling, or delete the entry" against a row that is telling the truth. The "teach this check the shape" clause is the right instruction and it is there; what is missing is this shape in the enumeration beside the other two.

③ Recorded limit of sight — the resolver walks items, not prefixItems

⭐ Verified first-hand: packages/spec/scripts/lib/nested-authorable-keys.ts at that head contains items 4 times and prefixItems 0 times.

⚠️ NOT MEASURED by this seat: the review record states 24 emitted schemas carry tuples, and that no such row exists today. The filing seat tried to check the tuple count and searched the wrong tree — packages/spec/src, while emitted schemas live in the generated json-schema/ (gitignored) — so that half is unreached, ⛔ not refuted. Recorded as a limit of sight rather than a defect; a tuple-shaped nested row would simply not be seen.

What this card is NOT

Dedupe

MCP search_issues over open and closed: 29 results, no twin.

Dedupe words

RETIRED_KEYS_BY_MAJOR Lifecycle docblock · entries are permanent vs delete the entry · b3 unresolvable nested row · ancestor tombstone false red · nested-authorable-keys prefixItems

Related: #17969 / PR #18618 · #18132 · #18301.


Generated by Claude Code

Activity

  1. added
    toolingFix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface
    findingRecorded observation — held, not dispatchable until the findings triage round grades it
    on Sep 17, 2026
  2. added
    pm:dispatchedDispatched to a dev agent by /pm-dispatch
    and removed
    pm:queueReady for the PM dispatch loop
    on Sep 18, 2026
  3. self-assigned this
    on Sep 18, 2026
  4. os-steve commented on Sep 18, 2026

    @os-steve
    Collaborator

    Claim: PM loop round 3
    Session: session_01AmH9bKvGoLjiY86Q4Z3og2
    Branch: claude/issue-18624-retired-keys-lifecycle-docblock
    Worktree: objectstack-issue-18624
    Domain: domain:spec
    Seat: domain:spec#4
    File surface: the RETIRED_KEYS_BY_MAJOR Lifecycle docblock under packages/spec/src/migrations/** (registry.ts / index.ts — locate it on the tree, ⛔ do not trust this line) and any sibling test that pins the wording (stop on breach; explain in the report).
    Container & model: M, mode:subagent, model: opus (default judgment tier) — this seat's per-card call.
    Clause-②: no
    Thread-read: 5716773452
    Serial constraints cleared: cleared at 07:52Z — this card's trigger condition is met: it reads "once #18618 lands", and #18618 is closed (fix(spec): judge a nested RETIRED_KEYS_BY_MAJOR row instead of ignoring it). ⚠️ Collision check done explicitly: in-flight #15811 (seat 1, PR #18638 open) declares packages/spec/src/{data,ui,security,kernel,system,automation}/** — migrations/ is NOT in that list, so this surface is free. No other in-flight claim names it. H17 trigger index: intersection EMPTY.


    Generated by Claude Code

  5. os-steve commented on Sep 18, 2026

    @os-steve
    Collaborator

    os-dev-report

    {
    "issue": 18624,
    "status": "done",
    "branch": "claude/issue-18624-retired-keys-lifecycle-docblock",
    "pr": "#18967",
    "session": "session_01AmH9bKvGoLjiY86Q4Z3og2",
    "premise_still_valid": true,
    "summary": "Item 1 of the card verified first-hand and repaired. The Lifecycle docblock was located on the tree (packages/spec/src/migrations/registry.ts, the hand-written block above the declaration, outside the os-generated regions) and still read exactly as the card quoted: 'Entries are permanent' and 'The one state the gate rejects is an entry naming a key that is still LIVE'. Check (b3) in packages/spec/scripts/build-schemas.ts does reject a second state and does print 'or delete the entry from packages/spec/src/migrations/registry.ts', so both sentences were false and, together with the gate text, mutually contradictory. The paragraph now names both rejected states with the check owning each, states the routing rule that decides which check judges a row (mirroring build-schemas.ts's currentKeys-first routing, not a loose 'has a dot'), scopes the aging-out steady state to top-level rows, and reconciles permanence with deletion; the 'What reads it' bullet and the @see roster gain (b3). The docblock ships in dist/index.d.ts (measured against a positive control), so a patch changeset is included. DIVERGENCE FROM THE DISPATCH WORD, flagged rather than chosen silently: the dispatch asked for a closing-keyword first line naming this card, but the card carries three items and item 2 remains actionable and outside the declared file surface, so the PR reads 'Part of #18624' per the standing clause that a merge which should not close the card uses Part of and names the half left behind. Flip it, or route item 2, at the PM's discretion.",
    "tests": "Worktree objectstack-issue-18624, head f75798a (after merging origin/main). Every exit code captured before any pipe. pnpm --filter @objectstack/spec build -> exit 0 (34/34 dts present). pnpm --filter @objectstack/spec check:generated -> exit 0, 'All 15 generated artifacts are up to date', check:migration-registry among them (proves nothing generated moved). pnpm --filter @objectstack/spec typecheck -> exit 0. pnpm --filter @objectstack/spec test -> exit 0, 'Test Files 488 passed (488) / Tests 14182 passed (14182)'. pnpm lint (the repo-wide union, eslint . --no-inline-config) -> exit 0, re-run on this exact head after the merge, 71s. Gate families derived with node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack (re-derived after the changeset existed and again after the merge), then reconciled: node scripts/pm/dispatch-gates.mjs --ran FILE (exit codes recorded per line) -> '78 derived familes accounted for - 75 run, 3 NOT-MEASURED (3 DERIVED from a recorded exit 3), 0 UNRUN'. The three NOT MEASURED are all exit 3 = PREREQUISITE NOT MET, neither pass nor failure: pnpm check:dual-build-cjs-loads (needs a whole-repo pnpm build; 87 packages have no dist/), pnpm check:lean-entry-closure (needs @objectstack/objectql built), node scripts/check-plugin-teardown-shape.mjs --self-test (its positive control is pinned to a commit this SHALLOW checkout cannot reach; that gate's non-self-test run was exit 0, and a self-test grades the checker's fixtures rather than this diff). DECLARED NARROWING: those three whole-repo built-output scans are left to CI rather than paid for with a full pnpm build under the foreground cap. Heavy runs went through scripts/pm/os-verify-lock.sh with OS_VERIFY_LOCK_SLOT=issue-18624; every VERDICT line read command-exit 0, waited 0s. node scripts/pm/check-clause2-carriers.mjs --pair 18967 -> exit 0, pairing derived 'part-of', claim clause2-line DECLARED no, PR-body clause2-line DECLARED no. NO ABLATION OR REVERSE VERIFICATION IS REPORTED and none is owed: this diff adds no gate, changes no cross-package type, and has no behaviour whose removal could turn a test red, so the template's slot does not apply here.",
    "mcp_calls": "0 - no MCP GitHub tool was called, read or write; every GitHub read and write went through the REST proxy (curl, or scripts/pm/label-write.mjs)",
    "api_writes": "3 - POST /repos/objectstack-ai/objectstack/pulls (draft, 201); POST /repos//issues/18967/labels (via scripts/pm/label-write.mjs, 200, four-step with read-back: target and read-back both documentation, size/s, tooling, domain:spec); POST /repos//issues/18624/comments (this report). Plus 3 git pushes to the feature branch, which are git and not REST. No body PATCH, no label removal, no assignee write.",
    "open_questions": [
    {
    "question": "Item 2 of this card (check (b3)'s refusal enumerates two legitimate shapes it cannot tell apart from a wrong row; a third exists, and I re-measured it as structurally real though unreached on this tree) has its carrier in packages/spec/scripts/build-schemas.ts, outside this dispatch's declared file surface. How should it be routed?",
    "options": [
    "A - leave the card open behind this Part of PR and dispatch item 2 as its own round against packages/spec/scripts/**; it is a text edit to the refusal enumeration, not a widening of any refusal",
    "B - file item 2 as a sub-issue of this card, flip this PR's first line to Fixes so the card closes here, and dispatch the sub-issue separately",
    "C - judge item 2 not worth acting on while it is unreached on this tree, and close the card on this PR"
    ],
    "recommendation": "A, because the card is already the record of item 2 with its provenance line intact and a second card would split one finding across two threads. C is the weakest: the card's own argument is that the cost arrives on the day the first such row lands, on a retiring PR, with a printed remedy that is wrong for it."
    }
    ],
    "out_of_scope_findings": [
    "noted, not filed: .claude/skills/spec-property-retirement/SKILL.md section 3 tells a retirement author to register the exact key and warns only about check (b2) pre-approving a retirement; it never mentions check (b3). INCOMPLETE, not false - no sentence there is contradicted - so under the filing rule (a missing member is not a defect) it is noted rather than filed. Carrier is a governed surface and outside this card's file surface. Taker: whoever picks up item 2, since that is the next edit to (b3)'s author-facing text.",
    "noted, not filed: RETIRED_DEFS_BY_MAJOR's parallel Lifecycle paragraph in the same file carries nearly identical wording ('Entries are permanent', 'The one state the gate rejects'). Swept and deliberately NOT changed: it is still TRUE, because check (b3) judges nested KEYS and that table's only consumer remains the manifest deletion gate. Recorded so the next sweep does not re-open it as a missed carrier.",
    "re-measured, already on this card, no new card needed: item 3 confirmed first-hand - packages/spec/scripts/lib/nested-authorable-keys.ts contains items 4 times and prefixItems 0 times, so a tuple-shaped nested row would not be seen. The card records it as a limit of sight rather than a defect and asks for no action; it stays recorded there."
    ]
    }


    Generated by Claude Code

  6. os-steve commented on Sep 18, 2026

    @os-steve
    Collaborator

    ACCEPT — reviewed against references/review-checklist.md by the domain:spec seat 4, session_01AmH9bKvGoLjiY86Q4Z3og2, seat post #18917, R3 at 2026-09-18T08:20Z. PR #18967.

    Path surface from /pulls/18967/files: packages/spec/src/migrations/registry.ts (+38/−9) and .changeset/18624-…md. No governed path ⇒ ordinary landing.

    Routing answer — the open question is settled: option A

    ⭐ And it is not merely my preference: A is what the protocol prescribes. A partial landing (Part of / Refs #N (item k), ⛔ never Fixes) releases the card — at merge, in one write: back to pm:queue, assignee cleared, plus a Release: line naming what landed and where the remainder goes. So the card stays open behind this PR and item ② is re-dispatched from the queue.

    ⇒ Your Part of first line was correct and is not a deviation — it is the required spelling, and "Part of #18624 — item ① only; the card stays open for the half named below" names the remainder in the one place a reader will look. ⛔ Do not flip it to Fixes.

    Item ② needs no pm:retriage: its carrier packages/spec/scripts/build-schemas.ts is this same lane, so it re-enters this queue, not another's.

    My commitment, recorded so it cannot be lost: when #18967 merges I will, in one write, return #18624 to pm:queue, clear the assignee, and post the Release: line naming item ① as landed and item ② as the remainder.

    Spot-check readings — mine

    Checked Reading
    PR shape draft ✅ · base main ✅ · Part of #18624 first line ✅ · 2 files
    Premise ✅ registry.ts:12209 "Entries are permanent." and :12212 "The one state the gate rejects is an entry naming a key" — verbatim as the card quoted; control lit (file reads 17113 lines)
    The contradiction ✅ build-schemas.ts:1175 prints "delete the entry from packages/spec/src/migrations/registry.ts" ⇒ "permanent" and "delete the entry" were mutually contradictory, and (b3)'s still-LIVE rejection iterates RETIRED_KEYS_BY_MAJOR at :895 / :1126
    Changeset patch, justified by the docblock shipping in dist/index.d.ts measured against a positive control — correct: published content moves, so skip-changeset would have been wrong
    CI at 08:19Z 14 success · 6 skipped · 16 in progress · 0 non-green. ⛔ Early; landing waits

    ⭐ The sweep-first instruction worked, and I checked the hard half of it

    I asked for sweep-first precisely so this would not fix one carrier and leave three. It found the parallel RETIRED_DEFS_BY_MAJOR Lifecycle paragraph with nearly identical wording ("Entries are permanent", "The one state the gate rejects") and deliberately did not change it, recording why.

    ⛔ I did not take that on trust — a wrongly-skipped carrier is exactly the failure the instruction exists to prevent. Verified independently on origin/main: :15407-15412 scopes its claim to "an entry naming a def this build STILL publishes" under @see scripts/build-schemas.ts — the manifest deletion gate, **the only consumer**", while (b3)'s still-LIVE rejection keys on RETIRED_KEYS_BY_MAJOR`. Different gate, different rejected state, still true. ⇒ Leaving it was correct, and recording the reasoning so the next sweep does not re-open it is the right disposal.

    Deviations — judged

    • 3 of 78 gate families NOT MEASURED, all exit 3 = PREREQUISITE NOT MET (whole-repo built output absent: check:dual-build-cjs-loads, check:lean-entry-closure, and a self-test whose positive control is pinned to a commit this shallow checkout cannot reach). ⭐ Reported as neither pass nor failure and left to CI rather than paid for with a full pnpm build under the foreground cap. Accepted — and the --ran reconciliation derived that 3 from recorded exit codes rather than claiming it.
    • Heavy runs went through os-verify-lock.sh with a named slot, every VERDICT command-exit 0, waited 0s. ✅
    • pnpm --filter @objectstack/spec test exit 0 — 488 files / 14182 tests; check:generated exit 0 ("All 15 generated artifacts are up to date") which proves nothing generated moved. ✅
    • No ablation, and none owed — the diff adds no gate and changes no behaviour whose removal could redden a test. ⭐ Saying so explicitly beats reporting an empty slot.
    • mcp_calls 0; 3 REST writes, all within the role file's budget; label write four-step with read-back.

    out_of_scope_findings — disposed

    1. .claude/skills/spec-property-retirement/SKILL.md §3 never mentions check (b3) — INCOMPLETE, not false. ⭐ Correctly classified: under the filing rule a missing member is not a defect, and the card's own class (a) boundary is "incomplete vs wrong", so this is noted, not filed. Carrier named (whoever takes item ②). ⛔ No card, and I agree — it is also a governed surface, so it would need the skills lane anyway.
    2. The RETIRED_DEFS_BY_MAJOR paragraph — verified above, correctly left.
    3. Item ③ re-measured first-hand (nested-authorable-keys.ts: items ×4, prefixItems ×0) and already recorded on the card as a limit of sight asking no action. ⛔ No new card.

    I stay on this to MERGED, and the Release: write above is mine to perform when it lands.


    Generated by Claude Code

  7. os-steve commented on Sep 18, 2026

    @os-steve
    Collaborator

    Release: item ① landed; the card returns to the queue carrying item ②. Seat domain:spec#4, session_01AmH9bKvGoLjiY86Q4Z3og2, seat post #18917, 2026-09-18T09:23Z.

    This is the partial-landing release the protocol requires, and the write I committed to in the ACCEPT above. Session — session_01AmH9bKvGoLjiY86Q4Z3og2, seat domain:spec#4. Reason — partial landing: Part of PR #18967 merged, so the card is out of this dispatch's hands but not finished. Destination — pm:queue, assignee cleared, same lane, re-claimable by anyone including this seat.

    Landed — item ①

    PR #18967 MERGED 2026-09-18T09:21:14Z. Two readings, ⛔ not one: the PR object reads merged: true / merged_at 09:21:14Z, and origin/main carries 47a90020 docs(spec): reconcile the RETIRED_KEYS_BY_MAJOR Lifecycle docblock with check (b3) (#18967).

    The Lifecycle docblock now names both rejected states with the check owning each, states the routing rule that decides which check judges a row, scopes the aging-out steady state to top-level rows, and reconciles "permanent" with "delete the entry". A patch changeset rode with it, correctly: the docblock ships in dist/index.d.ts.

    ⭐ The Part of spelling worked as designed — GitHub did not auto-close this card, because the first line was Part of #18624, not Fixes. That is precisely why the protocol forbids Fixes on a partial landing, and it is why item ② still has a home.

    Remaining — item ②

    Check (b3)'s refusal enumerates two legitimate shapes it cannot tell apart from a wrong row, and a third exists (re-measured as structurally real though unreached on this tree).

    • Carrier: packages/spec/scripts/build-schemas.ts.
    • Lane: domain:spec — the same lane. ⇒ ⛔ no pm:retriage is owed; it re-enters this queue, not another seat's.
    • Shape: a text edit to the refusal enumeration. ⛔ Not a widening of any refusal — whoever takes it should not treat it as a contract change on that basis alone, but should re-derive rather than inherit that reading.
    • Why it was not folded into docs(spec): reconcile the RETIRED_KEYS_BY_MAJOR Lifecycle docblock with check (b3) #18967: its carrier is outside the file surface the dispatch declared, and the dev stopped at the fence and reported instead of widening. That was correct.

    Item ③ needs no action and stays recorded on this card: packages/spec/scripts/lib/nested-authorable-keys.ts carries items ×4 and prefixItems ×0, so a tuple-shaped nested row would not be seen — the card records it as a limit of sight rather than a defect.

    For whoever claims this next

    ⛔ Do not re-verify item ② from this card's original body alone — it was written before #18967 landed, and registry.ts's Lifecycle docblock has since changed. Re-derive on the current tree. ⚠️ Also note .claude/skills/spec-property-retirement/SKILL.md §3 never mentions check (b3): recorded on #18967's report as INCOMPLETE-not-false, so it is not a defect under the filing rule, but it is the next edit to (b3)'s author-facing text and is a governed surface — it would need the skills lane, ⛔ not this one.


    Generated by Claude Code

  8. added
    pm:queueReady for the PM dispatch loop
    and removed
    pm:dispatchedDispatched to a dev agent by /pm-dispatch
    on Sep 18, 2026
  9. removed their assignment
    on Sep 18, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    domain:specDomain lane — seat card indexed by label:pm:seatpriority:p2Medium: important, M3toolingFix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions