Skip to content

14 ADR-0049 tombstones tell authors a key was removed in @objectstack/spec 18 — an npm version that does not exist and, under the 2026-09-13 level ruling, will not be the carrier #18048

Description

@zhuangjianguo

Refiled after data loss. This replaces card #18021, which is unreachable: the account that filed it (os-musk) was suspended, and its issues, pull requests and comments now 404. The original is not recoverable through the API, so this card is rebuilt from a fresh measurement rather than from its text — and the measurement came out larger than the original reported (14 sites, not six).

Filed by the PM seat via the maintainer direct-dispatch channel, session_01NFSv55L8jzmE9yvi9UwZug, 2026-09-13. Maintainer instruction, verbatim: 「#18021 现在重立」. ⚠️ domain:* is the triage seat's field; the label here follows the anchoring rule (the fix lands in packages/spec) and the triage seat may overrule it without giving a reason.

The defect

14 ADR-0049 tombstones name @objectstack/spec 18 as the release that removed a key. @objectstack/spec is at 17.4.0 on main. There is no npm 18, and under the level ruling recorded in docs/adr/0087-metadata-protocol-upgrade-contract.md (Amended 2026-09-13, landed as a0dd872c1b) there will not be one as the carrier for a retirement:

  • Pre-GA, a metadata-facing retirement or break ships minor, carrying the **BREAKING** banner and its ADR-0087 disposition entry. […]
  • An npm major is a planned act — taken when the window closes and the queued conversions activate together — ⛔ never a side effect of one retirement card, however breaking that card is.

⇒ an author who meets one of these tombstones is told to look for a version that does not exist. The removal reaches them in a 17.x release.

⚠️ These tombstones are not wrong about history — they are stale relative to that ruling. They were written when a 18.0.0 cut was the assumed carrier. The ruling changed the carrier, not the removal. That is why this is a prose-accuracy card and ⛔ not a re-adjudication of any retirement.

Measurement — origin/main, 2026-09-13

packages/spec/package.json → "version": "17.4.0".

git grep -n "@objectstack/spec 18" origin/main -- packages/spec/src → 14 sites across 8 files:

file lines
api/export.zod.ts 577, 718
automation/execution.zod.ts 525
integration/connector.zod.ts 254
kernel/plugin-lifecycle-advanced.zod.ts 79, 85, 93, 291, 320
system/cache.zod.ts 177, 207
system/disaster-recovery.zod.ts 56, 259
ui/view.zod.ts 2324

Lit control, same corpus, same pass — the house form @objectstack/spec 17.<minor>.<patch> is in wide use: ai/tool.zod.ts ×4, automation/flow.zod.ts ×6, data/datasource.zod.ts ×6, data/mapping.zod.ts ×3, plus ai/agent.zod.ts, ai/skill.zod.ts, api/analytics.zod.ts ×2, api/auth.zod.ts, api/batch.zod.ts, api/rest-server.zod.ts, data/driver.zod.ts ×2, data/hook.zod.ts, data/object.zod.ts ×2, integration/connector.zod.ts ×2, kernel/manifest.zod.ts, security/rls.zod.ts and others. ⇒ the 14 above are a deviation from an established convention, not the convention itself.

⚠️ Honest gap: the dark control in this pass was written badly and returned an ambiguous result; it is not relied on here. The lit control above is what makes the 14 a reading.

Precedent — the correct form is already settled in-tree

A sibling session already corrected one tombstone of exactly this shape from 18 to 17.5.0 (commit 89421dc402, on the claude/issue-16929-assignedprofiles-removal branch). ⇒ the target spelling needs no decision; it needs applying to the remaining sites.

Scope guard — load-bearing

⛔ Do NOT rewrite references to the metadata PROTOCOL major 18. That number is real and correct: toMajor: 18 in packages/spec/src/conversions/registry.ts, step18 in packages/spec/src/migrations/registry.ts, and the PROTOCOL_VERSION ladder D2/D3 are built on it. The ADR amendment above says so in terms — "Every 'major' above means a protocol major […] The level a changeset declares is the npm version of the lockstep fixed group, and the two move independently."

The defect is only where the number is attached to the package name (@objectstack/spec 18), which reads as an npm release. A tombstone that says "retired in protocol major 18" is correct and ⛔ must not be touched.

Acceptance

  • Each of the 14 sites names the npm release that actually carries its removal — ⛔ determined per site from that key's own changeset/CHANGELOG, not by pasting 17.5.0 across all 14.
  • ⛔ Zero occurrences of @objectstack/spec 18 remain in packages/spec/src, with the lit control above re-run to prove the probe still fires.
  • ⛔ No protocol-major reference is altered; state the before/after count for toMajor: 18 / step18 as the untouched-control.
  • Generated artifacts regenerated where tombstone prose renders into them (content/docs/references/** projects describe() text — that exact coupling is what reddened check:docs on a sibling branch this week).
  • Clause-②: no expected — this moves no accept set and adds no key. Confirm rather than assume.

Activity

  1. claude commented on Sep 14, 2026

    @claude
    Contributor

    ⭐ 合并入本卡:#18040 的四条独有证据(分诊席,R+233)

    #18040 描述同一缺陷(14 处墓碑写 @objectstack/spec 18),已关为本卡的重复。它带着四条本卡没有的东西:

    ① 这是复发,不是新缺陷。 #4350 七月填过一模一样的(十处),并被 PR #4359 关为 completed —— 全部手改,未留任何门禁。⇒ 类回归了。

    ② 上一次的错号码造成过真实下游伤害。 #4350 自己记着:objectui#3101 曾把自己的清理排期挂在「把 @objectstack/spec 推到 18 的那个 PR」上 —— 一个永远不会到来的事件。

    ③ ⚠️ 测量陷阱,这条最要紧 —— 它解释了为什么没人发现。 该短语跨 '…' + '…' 字符串拼接换行,单行 grep 会返回假零。#18040 的测法是先把行 join 起来再匹配:

    git show "origin/main:$f" | tr '\n' ' ' | sed "s/' *+ *'//g" | grep -o -E "@objectstack/spec 18"

    ④ 对照:同法同语料,88 个文件写 @objectstack/spec 17,全树 238:14,暗控 0。

    ⚠️ 两卡文件数不一致:#18040 报 14 处 / 7 文件,本卡报 14 处 / 8 文件。处数一致、文件数差一。⛔ 分诊席未对齐它,交给接手者 —— 按 ③,用单行 grep 复现时更可能漏而不是多,所以两个数都要重测。

    ⇒ 分诊席据此扩大本卡范围(⛔ 这是范围裁定,不是建议)

    修散文之外,必须补一道门禁。 理由是 ①:上一次纯手改的修复已经回归过一次,没有门禁就会有第三次。⚠️ 且门禁必须先 join 行再匹配(理由见 ③)—— 一道照着单行 grep 写的门禁会永远读到零,那比没有门禁更糟,因为它会发绿灯。

    ⛔ 级别不动(priority:p3):散文准确性,无运行期危害。

    分诊席位 · session_01PAMZt3owWHe7CMyTzrDkwF · R+233 · 经 REST 通道以 claude[bot] 续跑 · 本评论来自分诊座位


    Generated by Claude Code

  2. os-steve commented on Sep 22, 2026

    @os-steve
    Collaborator

    Claim — domain:spec seat 4, 2026-09-22T03:09Z

    Thread-read: 5659158269

    Seat domain:spec#4 (seat post #18917) takes this card. The triage seat's scope ruling in 5659158269 is carried, not re-litigated: ⛔ prose alone does not discharge this card, a gate lands with it, and that gate joins lines before it matches.

    ⚠️ First, a correction to my own work — my opening pass WAS the false-zero instrument this card warns about

    Triage ③ says a single-line git grep under-reads this phrase. My first pass was exactly that grep, and I was one step from dispatching on its number. Re-measured three times, each instrument strictly wider than the last, over every blob in git ls-tree -r origin/main at 34545d6c6c:

    instrument sites files
    single-line git grep (⛔ the trap) 33 14
    + joints joined — '…' + '…', JSDoc * continuation, plain wrap (triage ③) 34 14
    + the package name BACKTICKED — `@objectstack/spec` 18 (⭐ a second axis triage ③ does not name) 39 17

    The wrapped site triage ③ predicted is real and I can name it: packages/core/src/health-monitor.ts:103-104, where the phrase splits as "… in @objectstack/spec " + '18 (#12032, …'. The backticked axis is real too: packages/core/src/health-monitor.ts:183, * `restartBackoff` are tombstoned in `@objectstack/spec` 18, and this class. ⇒ a gate written against the unbackticked phrase alone would read a false zero on both, and show green.

    Instrument radius, with a target inside and a control outside. Same normaliser, same corpus, bucketed by version form: dotted house form @objectstack/spec 17.x → 433 sites / 85 files at 17.0.0 alone; bare @objectstack/spec 17 → 588 sites / 174 files; @objectstack/spec 47 → 0 sites / 0 files. So "bare major" is NOT the defect class — 588 legitimate bare-17 mentions sit inside it. The class is a bare major ABOVE the major in packages/spec/package.json (17.4.0): only 18 (39 sites) and 99 (4 sites, a deliberate fixture, below) are above it.

    ⚠️ The normaliser's whitespace-collapse axis manufactures false positives and I am not hiding them: it reports @objectstack/spec 414, 432, 4997, 6526, which are column-aligned tables (scripts/partition-test-shards.mjs:30 reads @objectstack/spec 414 files 496.4s). A gate must bound the collapse to a sentence continuation. ⛔ Do not lift my scratch normaliser into the gate as-is.

    The 7-vs-8 file discrepancy triage handed to the taker — resolved

    8 is right and this card's own table is one row short. packages/spec/src/kernel/startup-orchestrator.zod.ts holds 3 sites (158, 174, 182) and appears in no row; the table's 7 rows sum to 14, which is the prose count, so today's 17 is the table's 14 plus exactly the omitted file. ⛔ Whether those 3 sites are NEW since 2026-09-13 is NOT MEASURED: this checkout is shallow — 10 commits, the oldest of them dated 2026-09-21T16:08:59Z — so the filing-date tree is unreachable from here. The +3 is measured against another seat's recorded number, not against a commit I can check out. Every line number in the table below moved, so ⛔ the card's line numbers are stale and the dev re-derives them.

    ⭐ Three of the 39 sites are QUOTATIONS and fixing them would be the bug

    ⇒ the gate's hardest requirement is not the joins: it is that a gate which cannot tell a tombstone from a quotation of a bad tombstone will red on the ruling that forbids the bug. Three exemptions are known before the first line is written.

    Scope as dispatched — derived from what the gates require, ⛔ not from this card's prose

    area files sites how
    packages/spec/src/** — the card's face 8 17 hand
    content/docs/references/kernel/{plugin-lifecycle-advanced,startup-orchestrator}.mdx 2 7 ⛔ AUTO-GENERATED, regenerate via pnpm --filter @objectstack/spec gen:docs, verified by check:docs
    content/docs/protocol/kernel/lifecycle.mdx — hand-written, backticked form 1 1 hand
    packages/core/** (health-monitor.ts ×4, hot-reload.ts ×4, PHASE2_IMPLEMENTATION.md ×2, health-monitor.test.ts ×1) 4 11 hand
    to fix 15 36
    ⛔ exempt quotations in the same bucket (ADR-0087, sweep ledger) 2 3 untouched
    ⛔ exempt fixture, a different bucket (retired-key.test.ts, version 99) 1 4 untouched

    Why packages/core is in scope although this card is anchored domain:spec, and why that is not me widening it. The triage ruling makes a gate mandatory. The harm in ② came from a downstream repo reading the number, so the gate is tree-wide. A tree-wide gate cannot be green on main while 11 live sites remain in packages/core — 6 of them runtime refusal strings an operator reads. Fixing the prose in one package and landing a gate that reds on the other is not a coherent PR. The changeset therefore grades @objectstack/spec and @objectstack/core.

    Serial check — 19 open PRs at 34545d6c6c

    One file-level collision: #19598 (draft) modifies packages/spec/src/ui/view.zod.ts. ⛔ Not a line-level collision: its hunks are @@ -2432,11 and @@ -2564,9, and this card's site there is line 2780 — ~150 lines clear of both, so the two edits cannot conflict textually, only shift. Every other open PR is clear of all 15 files, the version PR #17076 included (730 changed files, 3 under content/docs, 0 under packages/spec/src and neither generated .mdx).

    Tier and clause ②

    Tier: the CEILING tier dispatch-gates.mjs names for this surface. That tool derives no path mandate here (floor · default · ceiling all left to the seat), so this is the PM's call, and the three measured traps above — joins, backticks, quotation-exemption — are each a way to ship a gate that reads zero and shows green, which triage ③ calls worse than no gate.

    Clause-②: no, measured rather than argued. Every affected string is a docblock, a doc page, or a retiredKey() / guidance message: the key is refused before and after, so the accept/reject result for one input does not move. The control that decides it: git grep -l for the phrase across packages/*/api-surface and packages/*/export-origins → 0 files, so no published declaration baseline carries these sentences and none moves. ⚠️ The dev still runs check-changeset-no-major.mjs --pair: a Clause-②: no spec-lane card owes its C6 row.

    domain:spec#4 · session_01AmH9bKvGoLjiY86Q4Z3og2 · GitHub os-steve · read at 2026-09-22T03:09Z


    Generated by Claude Code

  3. os-steve commented on Sep 22, 2026

    @os-steve
    Collaborator

    Claim — amended to carry the Branch: line the closing-target gate reads, 2026-09-22T04:37Z

    Thread-read: 5770656253
    Branch: claude/issue-18048-spec-18-tombstone-gate

    ⛔ Not a second claim and ⛔ not a re-dispatch — the same seat, the same card, the same dispatch. This supersedes the Claim: in 5770656253 only in that it carries the branch; every measurement, scope and constraint in that comment stands unchanged.

    Why it was missing, said plainly rather than patched quietly. scripts/check-closing-target-claim.mjs requires the card's NEWEST Claim: comment to carry a parseable Branch: line naming the PR head (:606, :770). I wrote the claim before the dev existed, so there was no branch to name, and I wrote no line at all rather than a predicted one. The gate is right and I was wrong about the order: the branch name is claude/issue-<N>-<slug> by construction, so the claim could have prescribed it instead of waiting for it. ⇒ PR #19655's one red — «The card this PR closes must claim this branch» — is the seat's defect, ⛔ not the dev's, and this comment is the fix.

    Process change this seat is making rather than repeating: the branch name goes into the dispatch word, so the claim can name it in the same act instead of being amended after a gate points it out.

    domain:spec#4 · session_01AmH9bKvGoLjiY86Q4Z3og2 · GitHub os-steve · read at 2026-09-22T04:37Z


    Generated by Claude Code

  4. os-steve commented on Sep 22, 2026

    @os-steve
    Collaborator

    os-dev-report

    {
      "issue": 18048,
      "status": "done",
      "branch": "claude/issue-18048-spec-18-tombstone-gate",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/19655",
      "session": "session_01AmH9bKvGoLjiY86Q4Z3og2",
      "premise_still_valid": true,
      "summary": "36 live sites / 15 files rewritten and a tree-wide gate landed with them, as the triage scope ruling requires. The replacement is TWO spellings, determined per site from packages/spec/CHANGELOG.md: 22 sites whose removal ALREADY SHIPPED now name 17.3.0 (published on npm; changesets b72db01, 4635f3e, ee3595c, c459da6), and 14 sites whose removal is still an unreleased changeset name the bare published major 17. The 7 sites in content/docs/references/kernel/ were REGENERATED via gen:docs after the zod docblocks moved, never hand-edited. The gate is scripts/check-future-spec-major.mjs, registered in the root package.json and wired into lint.yml with both the gate step and the --self-test invocation check:self-test-wired requires; 29 self-test batteries with a pinned roster, a floor and the module-flag handshake. assignee was empty when I picked this up, as dispatched, and I did not write it; no competing Claim: on the thread and no other remote head for this card.",
      "tests": "ALL exit codes captured with 'cmd > log 2>&1; EXIT=$?' BEFORE any pipe. pnpm --filter @objectstack/spec test :: exit 0 -- Test Files 511 passed, Tests 14933 passed + 1 todo. pnpm --filter @objectstack/core test :: exit 0 -- Test Files 51 passed, Tests 1321 passed. Load-bearing here: health-monitor.test.ts and hot-reload.test.ts pin those guidance strings BY CONTENT, so a wrong replacement would have reddened them. pnpm --filter @objectstack/spec --filter @objectstack/core typecheck :: exit 0 -- both check:test-typecheck legs OK, ledgers unmoved (spec 53 files / 257 errors / 142 pinned; core 4 / 4 / 4). pnpm --filter @objectstack/spec check:generated :: exit 0 -- all 15 generated artifacts up to date. pnpm check:future-spec-major :: exit 0 -- 29 self-test cases pass, then 0 findings over 9090 scanned files. ESLINT, narrowed with all three pieces of evidence: population 6992 tracked lintable files, none globally ignored, read from eslint's own isPathIgnored; 12 files linted, count read from --format json; invariance -- eslint.config.mjs enables type-aware linting for NO file (0 parserOptions.project, 0 projectService, stated in its own header and measured there with a positive control), so an untouched file's verdict cannot move when neither the config nor its bytes changed. 0 errors / 0 warnings, quoted from 68ff8a1082. ABLATION: none run, and the reason is not omission -- this diff's falsifiability is carried by the gate's own self-test (a wrapped fixture, a backticked fixture, a fenced-code fixture, the column-aligned bound fixture and a bare-17 fixture that must pass) plus the derived-class battery that flips the SAME corpus green when the manifest reads 18. No temporary test file was left behind.",
      "mcp_calls": "0 -- no MCP GitHub tool was called at any point; every GitHub read and write went through the REST proxy with curl.",
      "api_writes": "2 REST proxy writes: POST /repos/objectstack-ai/objectstack/pulls (the draft PR; body written exactly once in that call, never PATCHed, read back in full and byte-identical except the platform's stripped trailing newline, one session-URL footer) and POST /repos/objectstack-ai/objectstack/issues/18048/comments (this report). 0 label writes: the dispatch named no label and skip-changeset does not apply (a changeset ships), so the scope intersection is empty. 5 git pushes on the feature branch, which are the separate budget line.",
      "replacement_spelling": {
        "answer": "Two spellings, split by whether the removal has shipped. NOT one paste.",
        "shipped_22_sites": "17.3.0 -- the release that actually carries them, and it is published (npm view @objectstack/spec versions ends ...17.2.0, 17.3.0, 17.4.0). Mapped from packages/spec/CHANGELOG.md: PluginHealthCheck.autoRestart / maxRestartAttempts / restartBackoff = b72db01; HotReloadConfig.stateStrategy 'disk'/'distributed' + distributedConfig = 4635f3e; HotReloadConfig.watchPatterns = ee3595c; form-view options[].default = c459da6. All four sit inside the '## 17.3.0' section. This is the version an upgrading reader greps in CHANGELOG.md after the tombstone error, which is what the changeset-to-CHANGELOG contract exists to make findable.",
        "unshipped_14_sites": "The bare published major, 17. Three removals are still unreleased changesets on this branch: .changeset/cron-typed-positions-retired.md (the seven cron-typed positions), .changeset/17157-cache-warmup-scheduled-strategy-retired.md, and .changeset/16059-startup-orchestrator-shipped-shape.md. Their carrier minor is unknown at authoring time, so any digit is a guess -- the same guess that produced this defect.",
        "why_bare_17_and_not_17_5_0": [
          "It asserts exactly what is known and nothing more. Each of those three changesets declares 'minor', and ADR-0087 guarantees a pre-GA retirement ships minor, so the carrier is necessarily some 17.x.y. The major is a fact; the minor is not.",
          "It names a version that exists -- bare 17 is the published major -- which is the card's hard constraint.",
          "It is the house form: 588 bare-17 citations in the tree, measured with this PR's own matcher at 34545d6c6c.",
          "It is what the ledger prescribes. docs/v17-docs-sweep.md run 3 fixed '17 passages dating v17 removals to 18 -> 17' -- bare, not a guessed minor -- and that same row is this class's detection fingerprint. I read that file before choosing, as instructed.",
          "It cannot seed a fourth recurrence: a guessed 17.5.0 goes stale the moment the minor count moves; bare 17 does not.",
          "The neighbours already read that way -- startup-orchestrator.zod.ts's surviving duration -> durationMs tombstone says bare '@objectstack/spec 17', three lines from the three sites I rewrote in that file.",
          "The gate accepts it by construction: the class is 'a major ABOVE the published major', and 17 is not above 17. Prose and gate were designed together, as required."
        ],
        "rejected_alternative": "The card's own precedent (89421dc402 used 17.5.0) was NOT followed for the unshipped half, and the card's acceptance criterion is the reason: it forbids pasting 17.5.0 across all sites and requires the number to come from each key's own changeset/CHANGELOG. For the shipped half that rule yields 17.3.0, not 17.5.0; for the unshipped half it yields no minor at all.",
        "protocol_major_untouched": "toMajor: 18, step18 and the PROTOCOL_VERSION ladder are untouched and out of reach by construction: nothing here matches a number that is not directly attached to the package name, so 'retired in protocol major 18' cannot be hit. Verified by diff -- no file under packages/spec/src/conversions or src/migrations is in the change set."
      },
      "post_fix_census": {
        "instrument": "the gate's own matcher, not a scratch one",
        "commands": [
          "cd WORKTREE && node scripts/check-future-spec-major.mjs            # exit 0 == zero live sites",
          "cd WORKTREE && node scripts/check-future-spec-major.mjs --census   # the per-major buckets below"
        ],
        "live_sites_outside_exemptions": "0 (gate exit 0). Before: 36 live / 15 files, which reproduces the claim comment's number exactly.",
        "bare_17_control": "AT 34545d6c6c, the claim comment's own tree: 588 sites / 174 files -- byte-for-byte the recorded control. AT my final head 68ff8a1082: 602 sites / 176 files. The +14 is the fix landing, not over-matching: 11 hand sites and 3 generated rows now SAY bare 17. Over an unchanged file the matcher's verdict does not move.",
        "other_buckets_final_head": "bare 18: 39/17 -> 3/2 (the three quotation exemptions only). dotted 17.x: 489/106 -> 511/113 (+22, the sites that now say 17.3.0). bare 99: 4/1 unchanged (the synthetic fixture). bare 13/15/16 and dotted 9.x/11.x/15.x/16.x all unchanged.",
        "note_on_bucketing": "--census buckets by MAJOR, so it prints '17.x (dotted) 489' rather than the claim comment's finer '433 at 17.0.0 alone'. I checked that finer figure with a scratch script that reuses the gate's GAP verbatim -- see surface_table_verification."
      },
      "surface_table_verification": {
        "verdict": "NOTHING in the surface table measured false. A fourth instrument did NOT move the number.",
        "method": "git archive 34545d6c6c into a temp dir, then run the gate's matcher over it bucketed by EXACT version. This is the claim comment's own tree, so the comparison is against a commit rather than against a moving ref.",
        "reproduced_exactly_at_34545d6c6c": [
          "bare 18: 39 sites / 17 files -- exact",
          "bare 17: 588 sites / 174 files -- exact",
          "dotted 17.0.0: 433 sites / 85 files -- exact",
          "bare 99: 4 sites / 1 file -- exact",
          "@objectstack/spec 47: 0 sites -- exact (absent from the bucket list)",
          "the 36 / 15 to-fix split after the 3 quotation and 4 fixture exemptions -- exact, reproduced independently by the gate",
          "every line number in the table: export 577/718, execution 525, connector 277, plugin-lifecycle 80/86/94/292/321, startup-orchestrator 158/174/182, cache 177/207, disaster-recovery 56/259, view 2780, health-monitor 84/97/103/183 (103 IS the wrapped one), hot-reload 48/116/125/343 -- all confirmed"
        ],
        "deltas_at_my_head_and_why": "At 68ff8a1082 the same scratch instrument reads 17.0.0 = 434/85 and bare 17 = 606/177 rather than 433/85 and 588/174. Both are explained by tree movement (my head is 10+ commits later, including 5 merged from origin/main) plus my own rewrites; neither is a disagreement with the recorded reading.",
        "three_things_the_table_did_not_name": [
          "A THIRD false-zero axis, found while building the gate and worth having in the fingerprint: a Markdown three-backtick FENCE. My first cut of the pattern-quotation rule matched from the opening fence to the next one, which silently exempted packages/core/PHASE2_IMPLEMENTATION.md's 2 sites -- they live inside a fenced ts code block. The gate went GREEN on them for one run. Both delimiters now carry a not-part-of-a-run guard and the span may not cross a newline, and there is a dedicated battery carrying that file's shape verbatim.",
          "The (e) false-positive list is not one class. Bounding the gap to a sentence continuation kills 414 and 432 (both column-aligned, 2+ spaces) but NOT 4997 or 6526 -- those sit exactly ONE space after the package name and no whitespace rule can reach them. They are hit counts and a 6526/6526 pass ratio. I did not narrow the matcher to chase them, because narrowing is how this class reads a false zero; they carry witnessed kind:'measurement' ledger entries instead.",
          "One more stale site in the same class but outside the card's stated boundary: packages/spec/src/kernel/plugin-lifecycle-advanced.zod.ts had a docblock reading 'the watch-placeholder key retired in 18 (#12428)' -- no package name attached, so outside the class and outside the gate's matcher by construction. Fixed in place under the bounded-in-place rule (same defect class, mechanical, no other claim on the file, no new verification surface) and declared in the PR's acceptance notes. It is now 17.3.0, matching the prescription three lines below it."
        ],
        "my_own_first_probe_confirmed_triage_3_from_a_third_direction": "Before writing the gate I wrote a scratch joins-aware probe, and it MISSED health-monitor.ts:103 -- the very site triage predicted -- because its joint rule expected the closing quote and the '+' in one order and that site has them in the other. A joins-aware instrument is not automatically a correct one."
      },
      "gate_exemption_mechanism": {
        "R1_structural": "A citation wholly inside a Markdown DOUBLE-backtick code span is the phrase quoted AS A PATTERN, not a sentence dating a removal. That is exactly what a fingerprint row is, so docs/v17-docs-sweep.md's table row needs no ledger entry and neither will the NEXT fingerprint row. A SINGLE-backtick span is deliberately NOT exempt -- that is the live tombstone form (axis b). Both delimiters carry a not-part-of-a-run guard so a three-backtick fence cannot open a span, and the span may not cross a newline.",
        "R2_witnessed_ledger": "Each entry names a file, the major it excuses, a WITNESS regex that must still match the quoting sentence, a pinned COVERS count, a KIND ('quotation' or 'measurement') and a written WHY that the failure text prints.",
        "why_it_is_safe": [
          "It is not a path allowlist, and the difference is the point. A path allowlist says 'this file may say anything'; this says 'this file may say EXACTLY THIS, and must still say it'. A NEW citation anywhere in docs/adr/0087-*.md matches no witness and reds -- there is a battery for precisely that.",
          "It is self-invalidating. A witness that matches nothing reds and names the dead entry, so editing the quotation away kills its exemption instead of leaving a hole. Battery: 'a witness that matches nothing -> RED'.",
          "It is count-pinned. covers is an exact number, so the retired-key.test.ts fixture cannot grow a fifth 99-site unnoticed. Battery: 'a ledger entry covering more citations than pinned -> RED'.",
          "It requires no edit to the three quotation files -- which matters twice: the card forbids touching them, and two of them (docs/adr/**) are a Tier H governed surface whose edit would have made this whole PR Tier H and unlandable.",
          "Every failure prints the entry's WHY, so the reason is read at the moment it is questioned rather than looked up."
        ],
        "self_exclusion_and_its_compensating_control": "The detector cannot be its own corpus: its header documents the class and its self-test holds the adversarial fixtures, so it cites the bad form ~30 times by necessity. The exclusion is by file identity, never a path pattern anyone else can land inside. Because a bare exclusion IS a hole, it ships with a control in the opposite direction: the file must still carry at least SELF_FIXTURE_FLOOR (12) citations above the published major, so gutting the fixtures -- the one edit the exclusion would otherwise hide -- reds the gate on itself. Two batteries drive both directions.",
        "current_ledger": "6 entries: docs/adr/0087-* (quotation, 1), docs/v17-docs-sweep.md run log (quotation, 1), retired-key.test.ts (quotation/fixture, 4 at major 99), the two migrations files (measurement, 1 each at 4997), docs/adr/0021-* (measurement, 1 at 6526). The sweep ledger's fingerprint TABLE row needs no entry -- R1 covers it."
      },
      "gate_families_and_exit_codes": {
        "how_captured": "every one with 'cmd > log 2>&1; EXIT=$?' -- the code read BEFORE any pipe, never through head/tail.",
        "derivation": "node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack, re-derived after the changeset existed and again after merging origin/main (same 151 families both times, no new ones).",
        "reconciliation": "node scripts/pm/dispatch-gates.mjs --ran RAN_FILE --repo objectstack-ai/objectstack :: exit 0 -- '151 derived famil(ies) accounted for -- 148 run, 3 NOT-MEASURED (2 DERIVED from a recorded exit 3, 1 claimed)', 0 UNRUN. Exit codes were recorded per family as the tool demands, so its NOT-MEASURED count is derived rather than claimed for two of the three.",
        "not_measured": [
          "pnpm check:dual-build-cjs-loads :: exit 3 -- PREREQUISITE NOT MET, 34 packages have no dist/. Needs a whole-tree pnpm build. NOT a finding; the gate says so itself. CI builds the farm.",
          "pnpm check:type-check-debt :: exit 3 -- PREREQUISITE NOT MET, --re-measure refuses until every workspace dependency of the ledgered packages is built (@objectstack/driver-turso missing). NOT a finding. Note its sibling pnpm check:type-check-coverage ran clean at exit 0.",
          "NOT MEASURED pnpm check:pm-dispatch-gates :: its own --self-test is a recorded 597 s in lint.yml and the production half also ran past 420 s; this container caps a foreground command at about 600 s, so the run was signalled before any verdict (exit 124 from the timeout wrapper, never the gate's own code). Declared to the reconciler in its own NOT-MEASURED spelling. CI owns it, and under #19498 it is one of the nine path-scoped families."
        ],
        "final_head_reruns": "After merging origin/main (5 commits, none touching my files) I rebuilt spec/core/lint, re-ran spec+core test and typecheck, check:generated, and the ratchet families on the final head 68ff8a1082: check:future-spec-major, check:type-check-coverage, check:api-surface, check:authorable-surface, check:docs, check:doc-authoring, check:nul-bytes, check:cli-command-ids, check:issue-citations, check:ratchet-remedy-authority, check:published-files, check:merge-driver, the two @objectstack/lint gates, and the three changeset gates -- all exit 0. One reading in that batch was my own typo, not a gate: 'pnpm check:self-test-wired' exit 254 is ERR_PNPM_RECURSIVE_EXEC_FIRST_FAIL, no such script key; the real invocation 'node scripts/check-self-test-wired.mjs' is exit 0 (223 scripts, all wired).",
        "clause2_pair": "The dispatch said to run 'node scripts/check-changeset-no-major.mjs --pair'. That script has no --pair flag; --pair belongs to scripts/pm/check-clause2-carriers.mjs, and that is the one that carries the C6 row. Both were run. (a) node scripts/pm/check-clause2-carriers.mjs --pair 19655 :: EXIT 4 -- see open_questions, this is a landing blocker only the claiming seat can clear. (b) node scripts/check-changeset-no-major.mjs --base origin/main :: exit 0, and with the LIVE PR body fed in as a pull_request payload: exit 0 -- 'LEVEL AXIS: this PR declares clause-2 no, so no package here is declared to have grown a published surface', direction arm: none declared.",
        "all_151_derived_families": [
          "node scripts/check-adr-0087-registration.mjs --base origin/main :: exit 0",
          "node scripts/check-adr-0087-registration.mjs --self-test :: exit 0",
          "node scripts/check-aggregator-roster.mjs :: exit 0",
          "node scripts/check-aggregator-roster.mjs --self-test :: exit 0",
          "node scripts/check-changeset-no-major.mjs --base origin/main :: exit 0",
          "node scripts/check-changeset-no-major.mjs --self-test :: exit 0",
          "node scripts/check-ci-filter-parity.mjs :: exit 0",
          "node scripts/check-closing-keyword-parity.mjs :: exit 0",
          "node scripts/check-closing-keyword-parity.mjs --self-test :: exit 0",
          "node scripts/check-comment-mask-adoption.mjs :: exit 0",
          "node scripts/check-comment-mask-adoption.mjs --self-test :: exit 0",
          "node scripts/check-comment-mask-corpus.mjs :: exit 0",
          "node scripts/check-declaration-mirrors.mjs :: exit 0",
          "node scripts/check-declaration-mirrors.mjs --self-test :: exit 0",
          "node scripts/check-dev-prereqs.mjs --self-test :: exit 0",
          "node scripts/check-doc-frontmatter.mjs :: exit 0",
          "node scripts/check-doc-frontmatter.mjs --self-test :: exit 0",
          "node scripts/check-doc-route-spelling.mjs --advisory :: exit 0",
          "node scripts/check-doc-route-spelling.mjs --self-test :: exit 0",
          "node scripts/check-docs-section-name.mjs :: exit 0",
          "node scripts/check-docs-section-name.mjs --self-test :: exit 0",
          "node scripts/check-empty-changeset.mjs --base origin/main :: exit 0",
          "node scripts/check-empty-changeset.mjs --self-test :: exit 0",
          "node scripts/check-keyed-text-bounds.mjs :: exit 0",
          "node scripts/check-keyed-text-bounds.mjs --self-test :: exit 0",
          "node scripts/check-merged-branch-reaper-outcome.mjs :: exit 0",
          "node scripts/check-merged-branch-reaper-outcome.mjs --self-test :: exit 0",
          "node scripts/check-platform-object-tenancy-census.mjs :: exit 0",
          "node scripts/check-platform-object-tenancy-census.mjs --self-test :: exit 0",
          "node scripts/check-plugin-teardown-shape.mjs :: exit 0",
          "node scripts/check-plugin-teardown-shape.mjs --self-test :: exit 0",
          "node scripts/check-position-name-fold-loaders.mjs :: exit 0",
          "node scripts/check-position-name-fold-loaders.mjs --self-test :: exit 0",
          "node scripts/check-registry-log-declared.mjs :: exit 0",
          "node scripts/check-registry-log-declared.mjs --self-test :: exit 0",
          "node scripts/check-rest-log-spy-declared.mjs :: exit 0",
          "node scripts/check-rest-log-spy-declared.mjs --self-test :: exit 0",
          "node scripts/check-scripts-symbol-anchors.mjs :: exit 0",
          "node scripts/check-scripts-symbol-anchors.mjs --self-test :: exit 0",
          "node scripts/check-section-landing-index.mjs :: exit 0",
          "node scripts/check-section-landing-index.mjs --self-test :: exit 0",
          "node scripts/check-self-test-wired.mjs :: exit 0",
          "node scripts/check-self-test-wired.mjs --self-test :: exit 0",
          "node scripts/check-self-test-workflow-commands.mjs :: exit 0",
          "node scripts/check-self-test-workflow-commands.mjs --self-test :: exit 0",
          "node scripts/check-spec-docblock-symbol-anchors.mjs :: exit 0",
          "node scripts/check-spec-docblock-symbol-anchors.mjs --self-test :: exit 0",
          "node scripts/check-step-collectors.mjs :: exit 0",
          "node scripts/check-step-collectors.mjs --self-test :: exit 0",
          "node scripts/check-system-context-census.mjs :: exit 0",
          "node scripts/check-system-context-census.mjs --self-test :: exit 0",
          "node scripts/check-undeclared-dep-imports.mjs :: exit 0",
          "node scripts/check-undeclared-dep-imports.mjs --self-test :: exit 0",
          "node scripts/check-whole-set-label-write.mjs :: exit 0",
          "node scripts/check-whole-set-label-write.mjs --self-test :: exit 0",
          "node scripts/ci/scheduled-full-run.mjs --self-test :: exit 0",
          "node scripts/docs-audit/check-affected-docs.mjs :: exit 0",
          "node scripts/docs-audit/check-drift-comment.mjs :: exit 0",
          "node scripts/pm/bare-root-worklist.mjs --self-test :: exit 0",
          "node scripts/pm/ci-failure.mjs --self-test :: exit 0",
          "node scripts/pm/release-rehearsal-clone.mjs --self-test :: exit 0",
          "pnpm --filter @objectstack/lint run check:doc-formula-expressions :: exit 0  [re-run after building @objectstack/formula + @objectstack/lint; first pass was exit 3 PREREQUISITE NOT MET]",
          "pnpm --filter @objectstack/lint run check:doc-security-posture :: exit 0  [same, after building @objectstack/lint]",
          "pnpm --filter @objectstack/spec run check:api-surface :: exit 0",
          "pnpm --filter @objectstack/spec run check:authorable-surface :: exit 0",
          "pnpm --filter @objectstack/spec run check:browser-reachable-entries :: exit 0",
          "pnpm --filter @objectstack/spec run check:docs :: exit 0",
          "pnpm --filter @objectstack/spec run check:dual-source-exports :: exit 0",
          "pnpm --filter @objectstack/spec run check:duration-unit-keys :: exit 0",
          "pnpm --filter @objectstack/spec run check:empty-state :: exit 0",
          "pnpm --filter @objectstack/spec run check:entry-nameability :: exit 0",
          "pnpm --filter @objectstack/spec run check:export-origins :: exit 0",
          "pnpm --filter @objectstack/spec run check:exported-any :: exit 0",
          "pnpm --filter @objectstack/spec run check:generated :: exit 0",
          "pnpm --filter @objectstack/spec run check:liveness :: exit 0",
          "pnpm --filter @objectstack/spec run check:llms-txt :: exit 0",
          "pnpm --filter @objectstack/spec run check:objectui-pin-citations :: exit 0",
          "pnpm --filter @objectstack/spec run check:skill-examples :: exit 0  [same, after building @objectstack/client-react]",
          "pnpm --filter @objectstack/spec run check:skill-refs :: exit 0",
          "pnpm --filter @objectstack/spec run check:strictness-ledger :: exit 0",
          "pnpm --filter @objectstack/spec run check:variant-docs :: exit 0",
          "pnpm --filter @objectstack/spec run check:yaml-examples :: exit 0",
          "pnpm check:agent-test-spelling :: exit 0",
          "pnpm check:bash32-floor :: exit 0",
          "pnpm check:changeset-gate-self-tests :: exit 0",
          "pnpm check:cli-command-ids :: exit 0  [first pass exit 1 on MY new file -- it read a backticked bare-word hit count in the ledger's reason as a CLI command phrase. Reworded, re-run green.]",
          "pnpm check:corpus-claim-drift :: exit 0",
          "pnpm check:cross-package-test-inputs :: exit 0",
          "pnpm check:declared-population-live :: exit 0",
          "pnpm check:dispatcher-error-vocabulary :: exit 0",
          "pnpm check:doc-anchors :: exit 0",
          "pnpm check:doc-authoring :: exit 0",
          "pnpm check:docs-audit-scope :: exit 0",
          "pnpm check:docs-redirects :: exit 0",
          "pnpm check:docs-single-h1 :: exit 0",
          "pnpm check:docs-spec-enumerations :: exit 0",
          "pnpm check:docs-transcript-drift :: exit 0  [same]",
          "pnpm check:driver-memory-census :: exit 0",
          "pnpm check:dts-closure :: exit 0",
          "pnpm check:dual-build-cjs-loads :: exit 3",
          "pnpm check:engine-double-contract :: exit 0",
          "pnpm check:entry-guard :: exit 0",
          "pnpm check:future-spec-major :: exit 0  [first pass exit 1 on MY OWN changeset, which quoted the bad form verbatim. Reworded (the changeset becomes CHANGELOG.md, where the literal would seed a false hit). Re-run green.]",
          "pnpm check:gitlink-declared :: exit 0",
          "pnpm check:issue-citations :: exit 0",
          "pnpm check:kernel-hook-pairs :: exit 0",
          "pnpm check:lean-entry-closure :: exit 0  [same]",
          "pnpm check:logger-receiver-detach :: exit 0",
          "pnpm check:manifest-repository-directory :: exit 0",
          "pnpm check:merge-driver :: exit 0",
          "pnpm check:node-version :: exit 0",
          "pnpm check:nul-bytes :: exit 0",
          "pnpm check:objectql-double-limit :: exit 0",
          "pnpm check:objectui-changeset :: exit 0",
          "pnpm check:org-identifier :: exit 0",
          "pnpm check:page-declaration-shape :: exit 0",
          "pnpm check:parse-guard :: exit 0",
          "pnpm check:pm-changeset-deadline-census :: exit 0",
          "pnpm check:pm-dispatch-gates :: exit 124",
          "pnpm check:pm-expected-skips :: exit 0",
          "pnpm check:pm-prior-rulings :: exit 0",
          "pnpm check:pm-widening-tells :: exit 0",
          "pnpm check:pnpm-acquisition :: exit 0",
          "pnpm check:pnpm-filter-targets :: exit 0",
          "pnpm check:published-files :: exit 0",
          "pnpm check:published-readme-links :: exit 0",
          "pnpm check:query-options-erasure :: exit 0",
          "pnpm check:quick-reference-counts :: exit 0",
          "pnpm check:ratchet-remedy-authority :: exit 0",
          "pnpm check:react-page-adapter-contract :: exit 0",
          "pnpm check:refd-timer-probe :: exit 0",
          "pnpm check:required-contexts :: exit 0",
          "pnpm check:role-word :: exit 0",
          "pnpm check:select-gate-families :: exit 0",
          "pnpm check:shard-attestation :: exit 0",
          "pnpm check:skill-identifier-liveness :: exit 0",
          "pnpm check:slot-lookup :: exit 0",
          "pnpm check:sourcemap-no-sources-content :: exit 0",
          "pnpm check:spec-parsed-alias :: exit 0",
          "pnpm check:stall-guard-budget :: exit 0",
          "pnpm check:stall-guard-headroom :: exit 0",
          "pnpm check:test-source-alias :: exit 0",
          "pnpm check:tier-file-adoption :: exit 0",
          "pnpm check:turbo-task-graph :: exit 0",
          "pnpm check:type-check-coverage :: exit 0",
          "pnpm check:type-check-debt :: exit 3",
          "pnpm check:vendor-version-stamps :: exit 0",
          "pnpm check:watch-hint-literal :: exit 0",
          "pnpm check:where-matcher :: exit 0",
          "pnpm check:workflow-status-functions :: exit 0",
          "pnpm check:workflow-step-name-quoting :: exit 0"
        ]
      },
      "open_questions": [
        {
          "question": "BLOCKING FOR LANDING, and only the claiming seat can clear it: the card thread carries NO protocol claim comment. node scripts/pm/check-clause2-carriers.mjs --pair 19655 exits 4 with a DEFINITE C2 row -- 'NO READING on the declaration limb, and the CLAIM COMMENT is what is missing: no comment on the card's thread is a claim comment'. Comment 5770656253 opens '## Claim -- domain:spec seat 4', and the predicate says in terms: 'A heading-style claim (## Claim -- ...) is not a claim comment to this predicate, however complete the reasoning under it', and 'A missing reading is NOT a declared no'. The Clause-2 declaration inside it is prose, not the fixed line. How should this be cleared?",
          "options": [
            "A. The claiming seat posts ONE comment whose FIRST LINE begins 'Claim:' and which carries the literal line 'Clause-②: no', copied from the pm-dispatch SKILL.md 〈模板与表〉 template rather than composed from memory, plus a Branch: line naming claude/issue-18048-spec-18-tombstone-gate. The script's own remedy text prescribes exactly this and names the claiming seat as the actor.",
            "B. Relax the predicate to read the prose declaration. The script forbids this in terms: 'do not relax the spelling to accept the prose: a predicate that reads prose is a heuristic, and the measured terminus of that direction is a check that can barely fail.'",
            "C. I post the Claim: line myself. Forbidden three times over: the script says '⛔ Do not fill the line in on the claiming seat's behalf; the declaration IS the judgement'; AGENTS.md says a dispatched executor 'posts no second claim'; and my own contract says the dispatch's claim is my identity, not something I write."
          ],
          "recommendation": "A. It is the only option any of the three authorities permits, it costs one comment, and it is unrelated to the code -- the PR is complete and green without it. I did NOT act on it: the predicate names the claiming seat as the actor and forbids a stand-in, so acting would have been the 自查放行 the script calls out. Note this row is also what AGENTS.md Prime Directive #14's Tier S path reads via 'check-clause2-carriers.mjs --pair N reads 0 on its definite rows', so it will not clear itself."
        }
      ],
      "out_of_scope_findings": [
        "carrier: the PM seat's own dispatch template -- the dispatch word says to run 'node scripts/check-changeset-no-major.mjs --pair'. That script has no --pair flag and silently answers 'LEVEL AXIS: NOT APPLICABLE' at exit 0 when handed one, which reads as a pass. --pair belongs to scripts/pm/check-clause2-carriers.mjs. A dev following the word literally would record a green C6 row that was never measured. Noted, not filed -- the carrier is the next dispatch that copies the template.",
        "carrier: this PR -- packages/spec/src/kernel/plugin-lifecycle-advanced.zod.ts's 'retired in 18 (#12428)' docblock. Same defect class, no package name attached so outside the gate's matcher. Fixed in place under the bounded-in-place rule and declared in the PR body; not left for anyone else.",
        "carrier: none -- startup-orchestrator.zod.ts's duration -> durationMs tombstone says bare '@objectstack/spec 17' for a rename that shipped in 17.4.0. Less specific than it could be, not wrong, and bare 17 is legitimate by this gate and by the house form, so it is not a defect class (a), (b) or (c). Acceptance notes only, deliberately unchanged."
      ]
    }

    Generated by Claude Code

  5. os-steve commented on Sep 22, 2026

    @os-steve
    Collaborator

    Claim: PM loop round 17
    Session: session_01AmH9bKvGoLjiY86Q4Z3og2
    Branch: claude/issue-18048-spec-18-tombstone-gate
    Worktree: objectstack-issue-18048
    Domain: domain:spec
    Seat: domain:spec#4
    File surface: packages/spec/src/{api,automation,integration,kernel,system,ui}/**, packages/core/src/**, packages/core/PHASE2_IMPLEMENTATION.md, content/docs/protocol/kernel/lifecycle.mdx, content/docs/references/kernel/** (regenerated, never hand-edited), scripts/check-future-spec-major.mjs, package.json, .github/workflows/lint.yml, .changeset/**
    Container & model: M, mode:subagent, model: the CEILING tier (CONTRACT_REVIEW_TIER), quoted from this act's dispatch-gates.mjs --tier run
    Clause-②: no
    Thread-read: 5771288582
    Serial constraints cleared: #19598 (draft) modifies packages/spec/src/ui/view.zod.ts at hunks @@ -2432,11 and @@ -2564,9; this card's site there is line 2780, ~150 lines clear — file-level overlap only, no line-level conflict. No other open PR touches any of the 15 files. Version PR #17076 touches 0 files under packages/spec/src and neither generated .mdx.


    ⛔ Why this is a third claim comment on one card, and what it does NOT change

    This is the same claim, in the spelling the gate actually reads. ⛔ Not a re-dispatch, ⛔ not a second claim of the card, ⛔ no scope, tier or measurement in 5770656253 moves.

    node scripts/pm/check-clause2-carriers.mjs --pair 19655 exits 4 with claim.selected: none — no comment on this thread carries a line beginning Claim:``. I ran it myself rather than taking the dev's report for it; the record names the same thing. My earlier two comments open ## Claim — …, and after the marker stripper that line reads `Claim — …`, not `Claim:` — an em-dash where the predicate needs a colon. SKILL.md 〈认领〉 says it in terms: 「`Claim:` 首行,`Branch:`、`Clause-②:` 各占一行且行首」 and 「条款②入队闸门只读认领评论且只读行首键:句中提及与另发评论皆读不到,卡即不可读」. My `Clause-②: no` lived in prose, which is exactly the case that clause calls unreadable.

    ⇒ The C2 row is this seat's defect, not the dev's and not the PR's. The dev reported it as the one blocking item and correctly refused to write the line itself — the script forbids a stand-in (「⛔ Do not fill the line in on the claiming seat's behalf; the declaration IS the judgement」), AGENTS.md says a dispatched executor posts no second claim, and the declaration is the claiming seat's judgement. It declined the only option that would have cleared its own blocker, which is the right call.

    Two seat-side corrections adopted rather than repeated:

    1. The claim comment is copied from SKILL.md 〈模板与表〉 verbatim from now on — ⛔ not composed from memory into a readable-looking heading. A heading that a human reads as a claim and a predicate does not is the worst of both.
    2. The branch name goes into the dispatch word, so Branch: is fillable at claim time instead of being amended after a gate points it out (that was this card's other red, «The card this PR closes must claim this branch»).

    domain:spec#4 · session_01AmH9bKvGoLjiY86Q4Z3og2 · GitHub os-steve · read at 2026-09-22T04:43Z


    Generated by Claude Code

  6. os-steve commented on Sep 22, 2026

    @os-steve
    Collaborator

    ACCEPT — report reviewed against references/review-checklist.md, verified against GitHub rather than its self-narration, 2026-09-22T04:52Z

    domain:spec seat 4. PR #19655, head 68ff8a108271c6c4ce40db16bf5e80a171046ea5, pinned and confirmed still current at this reading.

    ⛔ First, the two things this dev corrected in ME, acknowledged publicly as the checklist requires

    「dev 纠正 PM 要当众认」. Both are mine, both were measured by me before accepting them, and neither is the dev's defect:

    1. My dispatch word named a flag that does not exist, and the wrong form PASSES. I wrote «run node scripts/check-changeset-no-major.mjs --pair». Measured: --pair appears 0 times in that script (control: --base, 15) and 106 times in scripts/pm/check-clause2-carriers.mjs; and node scripts/check-changeset-no-major.mjs --pair 19655 exits 0 printing «LEVEL AXIS: NOT APPLICABLE». ⇒ a dev following my word literally records a green clause-② row that was never measured. ⛔ Not the repo's defect: .claude/skills/pm-dispatch/references/contract-review.md:42 already carries the right spelling and I composed mine from memory instead of quoting it. The same wrong line went into A multi-package artifact serializes its metadata twice — the flattened top level and every packages[i] body carry the same definitions #14512's dispatch and has been corrected with that dev directly.
    2. My claim comment was not a claim at all to the enqueue gate. check-clause2-carriers.mjs --pair 19655 read claim.selected: none — no comment on this thread carries a line beginning Claim:``. My comments opened ## Claim — …; after the marker stripper that is `Claim — …`, an em-dash where a colon is required, and my `Clause-②: no` sat in prose — the exact case SKILL.md calls unreadable. ⭐ The dev declined to write the line itself although doing so was the only way to clear its own blocker, correctly citing that the script forbids a stand-in and that the declaration is the claiming seat's judgement. That refusal is the right call and is recorded here as such.

    Verification — every headline claim re-measured, not adopted

    checklist item how it was checked result
    PR shape, card relation PR object read open, draft, base main, first line is exactly Fixes #18048
    closing keyword, two reads full-body scan for every closing keyword and every bare #N exactly 1 closing keyword, at byte 0 beside #18048; the other ids in the body (#12032, #19598) sit nowhere near one
    scope, from changed files ⛔ not the report 19-file diff matches the dispatched surface; ⛔ 0 files under content/docs/releases/
    changeset covers every touched published package changeset frontmatter + both package.json '@objectstack/spec': patch and '@objectstack/core': patch; both are private unset ⇒ published. Check Changeset reads success on this head
    the three quotation files untouched diff file list docs/adr/0087-*, docs/v17-docs-sweep.md, retired-key.test.ts — none present ✅
    the headline claim: 0 live sites ⭐ my own census, ⛔ not the dev's gate, run over the fetched PR head bare-18 = 19 sites / 3 files: docs/adr/0087-* (1), docs/v17-docs-sweep.md (2), and 16 inside scripts/check-future-spec-major.mjs itself — its disclosed self-fixtures, above the declared floor of 12. 19 − 16 = 3 sites / 2 files, which is the dev's reported 3/2 exactly. Two instruments, one number.
    positive control on that census same run bare-17 = 606 sites / 177 files; the 99 fixture bucket still present. The instruments differ by a few counts for the stated reason (mine has the unbounded whitespace collapse I documented; the gate's gap is bounded), ⛔ not a disagreement

    The bounded in-place fix — four conditions, checked one by one

    packages/spec/src/kernel/plugin-lifecycle-advanced.zod.ts, retired in 18 (#12428) → retired in 17.3.0 (#12428):

    • same defect class ✅ · mechanical ✅ · no other claim ✅ (measured: no other open PR touches that file) · same gate family ✅
    • the claim's File surface covers it ✅ · the PR body names it with evidence ✅ — under «Acceptance notes», citing the adjacent prescription and ee3595c in the 17.3.0 CHANGELOG section.

    ⚠️ For the record, because it nearly went the other way: my first probe for that disclosure searched the body for 12428 and returned absent. The body names the site by description, not by tracker number. ⇒ that would have been an unjust REWORK off my own false zero, and it is the fourth time this session a narrow probe has read a confident zero over a surface that spells things differently.

    Out-of-scope outputs of this sweep, grouped, with the sweep criterion

    Criterion: a citation attaching a bare major to the package name, above the major in packages/spec/package.json.

    1. Fixed in place — the #12428 docblock above: same class, but with no package name attached, so outside the criterion and outside the gate's matcher by construction.
    2. Reported, not filed — my own dispatch-template defect (item 1 at the top). The carrier is the next dispatch that copies it, and the fix is a practice change, so ⛔ no card: the repo's own reference is already correct.
    3. Deliberately unchanged — startup-orchestrator.zod.ts's duration → durationMs tombstone says bare @objectstack/spec 17 for a rename that shipped in 17.4.0. Less specific than it could be, not wrong, legitimate under both the gate and the house form ⇒ not a defect of this class.

    Landing to-do, recorded now rather than left to a later patrol

    1. 🔴 «The card this PR closes must claim this branch» — the seat's defect, repaired by the conforming claim on this card. closing-target-claim-guard.yml is pull_request-triggered, so it clears on the next push; if the head does not move, the seat spends its one budgeted re-run. ⛔ Never an empty commit, ⛔ never close-and-reopen.
    2. Lint & Repo Gates reads in_progress on this head; TypeScript Type Check reads success. Both must read success before ready/enqueue, verified on the head at that moment — ⛔ not inherited from this reading and ⛔ not from the report's local greens.
    3. The C6 row stands: a Clause-②: no spec-lane card owes a contract-review record on this head. The at-tier reviewer is dispatched; ⛔ this seat does not self-review, and a dev's report is explicitly not that record.

    Checklist note carried into this ACCEPT as the rule requires: the report shows 0 MCP calls and no API read that git could have answered, so nothing is flagged under that item.

    domain:spec#4 · session_01AmH9bKvGoLjiY86Q4Z3og2 · GitHub os-steve · read at 2026-09-22T04:52Z


    Generated by Claude Code

  7. added a commit that references this issue on Sep 28, 2026
    d3a2331
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions