Repository navigation
pm(half-states): H19 founds every verdict on the set it judges - #18654
Merged
Merged
Conversation
H19 resolved `Blocked-by:` targets over the body∪comment union and judged against the body's set, but the UNJUDGED branch sat on the RESOLUTION step: a target superseded in the body and unreadable in the archive could still veto the verdict, producing a row no correct action by the card's owner could ever clear. Both founding states are now taken over the judged set. What the archive still does — and must, per #11747 — is withhold: an OPEN target it names still refuses a discharge, and an UNREADABLE one refuses a FULL discharge and is named in its own clause. Claude-Session: https://claude.ai/code/session_01Gqi43smmqjJ5sUrhfoPeKu Co-authored-by: Claude <noreply@anthropic.com>
This was referenced Sep 17, 2026
os-justin
marked this pull request as ready for review
September 17, 2026 12:51
This was referenced Sep 17, 2026
fix(pm): make the stamp contract quotable — Markdown code is a quotation, rendered as written
#18730
Merged
akarma-synetal
pushed a commit
to akarma-synetal/framework
that referenced
this pull request
Sep 28, 2026
…tack-ai#18681) Fixes objectstack-ai#18456 Clause-②: no `scripts/pm/` sits outside every workspace package, and the root package is private, so no `files[]` can ship this diff — `skip-changeset`. ## The defect `check-clause2-carriers --pair` is the landing pre-check every seat runs, and on one pair (PR objectstack-ai#17917 / card objectstack-ai#17425) it answered **0 at 02:57Z, 4 at 03:04:09Z and 0 at 03:58:33Z on 2026-09-13 with an identical script blob**. Two explanations were ruled out with controls (no comment on that thread was ever edited; the board is resolved from the environment, never from the working directory), so the cause is still UNKNOWN — and the three runs could not be compared, because not one of them had SAID what it read. The judging half is already deterministic given a fixed document (`--pair-json` proves that); what was unpinned is **what document the live path builds**. This states it: every `--pair` run now closes with a fenced `clause2 input record` block on stderr, with the same field roster on every exit, so two runs that disagree are settled by **diffing their two blocks** — never by re-running until one side wins. ⛔ No guess at the cause is dressed as a fix here: no predicate, no state, no row, no count and no exit code reads one character of the record, and the judging half is untouched. ## The record's field roster Rendered from `INPUT_RECORD_RUN_FIELDS` and `INPUT_RECORD_PAIR_FIELDS` and from nowhere else, so a field cannot silently disappear: a declared field this run could not fill renders with an explicit token rather than vanishing, and a field the builder fills that the roster does not declare is NAMED in the block (`record.undeclared`). Values too long for one line continue on indented lines under their key. | half | fields | |:--|:--| | run | `record.version` · `run.utc` · `run.mode` · `run.script.path` · `run.script.blob` · `run.script.bytes` · `run.node` · `board.repo` · `board.source` · `read.plan` · `read.api` · `read.token` · `read.served` · `read.pair-json` · `run.requests` · `pairs.derived` | | per pair (`pair.N.`) | `pr` · `card` · `derivation` · `head-sha` · `card-comments` · `card-comment-ids` · `card-comment-newest` · `pr-comments` · `pr-comment-ids` · `pr-comment-newest` · `claim.rule` · `claim.selected` · `claim.rejected` · `claim.clause2-line` · `pr-body.clause2-line` | Four of them are worth naming for WHY they are there: - **`run.requests`** — every read the run issued, in order, with its channel, its exact path and its **row count**. A page asked for with `per_page=100` that answers with exactly 100 rows is the one shape a truncated read and a complete one share, and nothing printed it. - **`claim.rule` + `claim.selected` + `claim.rejected`** — the carrier, the rule that picked it and every candidate it did not pick, each with its reason. That separates "the two runs selected different comments" from "the two runs applied different rules". - **`claim.selected`'s body fingerprint** (bytes + `sha256:`) — the field the measured 0/4/0 actually needs. A `misplaced` verdict on that thread requires the governing claim to have carried no readable declaration while a superseded one did; same ids with a different verdict is only possible if the BYTES differed, and the ids were all anybody could see. - **`run.script.blob`** — git's blob hash of this file, beside the path it ran from. "The blob was identical on both sides" was a claim in the incident; it is now a printed fact any seat checks with `git hash-object`. On this PR's head it reads `25d204236aa8296644813109fa77541d6efe1644`, which is exactly `git rev-parse HEAD:scripts/pm/check-clause2-carriers.mjs`. The `--json` sweep carries the same record under `inputs` — the same record, ⛔ never a second format. ## The two live blocks the card names `--pair 17917` — the pair from the card. Both it and objectstack-ai#18654 have since merged, so `--pair` answers **exit 2** on each today (the pair cannot be formed from a closed PR). ⭐ That is precisely the class of exit the old code said the least about, and the block is now complete on it: ```text ----- clause2 input record v1 ----- record.version: 1 run.utc: 2026-09-17T14:10:26.210Z run.mode: --pair 17917 run.script.path: /home/user/objectstack-issue-18456/scripts/pm/check-clause2-carriers.mjs run.script.blob: 25d2042 (git blob sha1 — check it with `git hash-object` on the path above) run.script.bytes: 513169 run.node: v22.22.2 board.repo: objectstack-ai/objectstack board.source: default — NEITHER PM_SWEEP_REPO NOR GITHUB_REPOSITORY answered read.plan: (i) token then (ii) token-less public read read.api: https://api.github.com (REST, accept application/vnd.github+json) read.token: present read.served: token=1, public=0, pair-json=0 read.pair-json: (not named — this run read the network) run.requests: 1 read(s), in the order they were issued objectstack-ai#1 (i) token /repos/objectstack-ai/objectstack/pulls?state=open&per_page=100&page=1 -> HTTP 200 (21 row(s)) pairs.derived: 0 pair(s) record.how-to-read: two runs that DISAGREE about one pair are settled by diffing their two blocks — ⛔ never by re-running until one side wins. The blob line says whether the two runs were even the same instrument. ----- end clause2 input record ----- ``` `--pair 18654` — the pair this seat landed today, which answered 0 at 12:32Z and is likewise merged now (**exit 2**): ```text ----- clause2 input record v1 ----- record.version: 1 run.utc: 2026-09-17T14:10:27.163Z run.mode: --pair 18654 run.script.path: /home/user/objectstack-issue-18456/scripts/pm/check-clause2-carriers.mjs run.script.blob: 25d2042 (git blob sha1 — check it with `git hash-object` on the path above) run.script.bytes: 513169 run.node: v22.22.2 board.repo: objectstack-ai/objectstack board.source: default — NEITHER PM_SWEEP_REPO NOR GITHUB_REPOSITORY answered read.plan: (i) token then (ii) token-less public read read.api: https://api.github.com (REST, accept application/vnd.github+json) read.token: present read.served: token=1, public=0, pair-json=0 read.pair-json: (not named — this run read the network) run.requests: 1 read(s), in the order they were issued objectstack-ai#1 (i) token /repos/objectstack-ai/objectstack/pulls?state=open&per_page=100&page=1 -> HTTP 200 (21 row(s)) pairs.derived: 0 pair(s) record.how-to-read: two runs that DISAGREE about one pair are settled by diffing their two blocks — ⛔ never by re-running until one side wins. The blob line says whether the two runs were even the same instrument. ----- end clause2 input record ----- ``` ⭐ `diff` of those two blocks is **four lines**: `run.utc` and `run.mode`, twice. Same roster, same order, same shape — which is the property the card asked for. ## A live block on exit 0 `--pair 18659` (open at the time of writing) — **exit 0**, the full pair half: ```text ----- clause2 input record v1 ----- record.version: 1 run.utc: 2026-09-17T14:10:36.744Z run.mode: --pair 18659 run.script.path: /home/user/objectstack-issue-18456/scripts/pm/check-clause2-carriers.mjs run.script.blob: 25d2042 (git blob sha1 — check it with `git hash-object` on the path above) run.script.bytes: 513169 run.node: v22.22.2 board.repo: objectstack-ai/objectstack board.source: default — NEITHER PM_SWEEP_REPO NOR GITHUB_REPOSITORY answered read.plan: (i) token then (ii) token-less public read read.api: https://api.github.com (REST, accept application/vnd.github+json) read.token: present read.served: token=5, public=0, pair-json=0 read.pair-json: (not named — this run read the network) run.requests: 5 read(s), in the order they were issued objectstack-ai#1 (i) token /repos/objectstack-ai/objectstack/pulls?state=open&per_page=100&page=1 -> HTTP 200 (21 row(s)) objectstack-ai#2 (i) token /repos/objectstack-ai/issues/18443 -> HTTP 200 objectstack-ai#3 (i) token /repos/objectstack-ai/issues/18443/comments?per_page=100 -> HTTP 200 (4 row(s)) objectstack-ai#4 (i) token /repos/objectstack-ai/objectstack/pulls/18659/files?per_page=100&page=1 -> HTTP 200 (1 row(s)) objectstack-ai#5 (i) token /repos/objectstack-ai/issues/18659/comments?per_page=100 -> HTTP 200 (1 row(s)) pairs.derived: 1 pair(s) pair.1.pr: 18659 pair.1.card: 18443 pair.1.derivation: `closing-keyword` (via a closing keyword) — body line: Fixes objectstack-ai#18443 pair.1.head-sha: 1344eb5 pair.1.card-comments: 4 row(s) pair.1.card-comment-ids: 5713976124,5714587497,5714873191,5715029659 pair.1.card-comment-newest: 5715029659 at 2026-09-17T13:19:56Z pair.1.pr-comments: 1 row(s) pair.1.pr-comment-ids: 5715030051 pair.1.pr-comment-newest: 5715030051 at 2026-09-17T13:19:57Z pair.1.claim.rule: the GOVERNING claim — the NEWEST comment whose body carries a line beginning `Claim:`/`Claimed:` AND whose `Branch:` line parses at least one protocol-shaped branch (newest by `created_at`; an unreadable stamp or a tie falls back to thread order, later row wins). The pool is every claim comment sharing that `created_at`; when NO claim names a branch at all, every claim comment is the pool. ⛔ Not earliest, ⛔ not a session match, ⛔ not the one whose body mentions the key. pair.1.claim.selected: 1 comment(s) in the pool 5714587497 at 2026-09-17T12:46:45Z — 2159 bytes, sha256:795e1df6c9fd pair.1.claim.rejected: none — every claim comment on this thread is in the pool pair.1.claim.clause2-line: DECLARED `no` — Clause-②: no pair.1.pr-body.clause2-line: DECLARED `no` — Clause-②: no⚠️ stated as an INPUT only — ⛔ no row here judges the PR body; the declaration limb is judged from the card, and `check-changeset-no-major.mjs` is what reads this line. record.how-to-read: two runs that DISAGREE about one pair are settled by diffing their two blocks — ⛔ never by re-running until one side wins. The blob line says whether the two runs were even the same instrument. ----- end clause2 input record ----- ``` ## Pins Battery **objectstack-ai#18456: the `--pair` input record — the same block on every exit, so two runs that disagree can be diffed**, registered in `SELF_TEST_BATTERIES` with a floor of **38**; **41** cases register. `SELF_TEST_BATTERY_FLOOR` raised 26 → 27 by exactly the one battery this adds. What is pinned, in the card's own terms: - the record is **present and complete on exit 0**, on the **exit-4 (MISPLACED)** shape and on a **refusal that formed no pair** — all three key lists asserted equal; - the **field roster** cannot lose a field: a declared field that was never filled still renders (with `INPUT_RECORD_UNSET`), an empty record still carries every declared key, and a key outside the roster is named rather than printed in silence; - the **selected-claim rule is stated**, and it is the one constant `claimCarrierSelection` applies — so the printed rule cannot drift from the applied one; - a **rejected candidate is named with its reason**, and a thread with nothing rejected says so; - a **`--pair-json` run names that read path as such** and names the document; - the body fingerprint **moves when only the bytes move** while every id field stays identical — the measured shape, asserted directly; - `gitBlobSha1` is pinned against two values `git hash-object` prints. ⛔ CONTROLS in the same battery: the block carries no verdict, no exit code and no finding row; building it changes no reading; and the selection the block prints IS the pool `cardDeclaration` judged (ONE derivation — `cardDeclaration` now calls `claimCarrierSelection` instead of deriving the pool inline, so the record and the verdict cannot describe two different comments). `--self-test` on this head: **786 cases pass, exit 0** (745 before; +41). ## Ablation From the committed tree, blob `25d204236aa8296644813109fa77541d6efe1644` (= this PR's head blob), the pair half of the record removed on disk, mutation proved before the run, restore by blob hash under a `trap`: ```text HEAD blob 25d2042 before: removed-text count=1 (want 1); injected count=0 (want 0) after : removed-text count=0 (want 0); injected count=1 (want 1) mutated blob e88355f70e8648f1e3d30147f0c82b7c3c157609 VERDICT ablation-mutated self-test exit=1 ← 14 cases red ✗ every declared PAIR field is present once per derived pair, prefixed by its index ✗ the SELECTION RULE is printed, not merely applied — two runs must be comparable on the rule too ✗ …and it is the one constant, so the printed rule cannot drift from the applied one ✗ the SELECTED carrier is named by id and by date ✗ ⭐ …with a BODY FINGERPRINT: the one field that tells "same ids, different bytes" apart ✗ ⭐ …and it MOVES when only the bytes move: same ids, same count, same newest, different verdict ✗ every REJECTED candidate is named, with the reason it is not the carrier ✗ …and a thread whose claims are all in the pool says THAT, rather than going quiet ✗ a claim that parses ZERO branches leaves NO carrier, and the block names that claim ✗ an UNREAD thread reads UNREAD, ⛔ never 0 rows ✗ the line READ from the carrier is stated — declared, near miss or nothing ✗ the PAIRING quotes the body line it was derived from ✗ …and the branch-name fallback names the head ref instead of quoting a line that does not exist ✗ the PR-BODY line is read and stated — ⛔ and stated as an INPUT, never as a limb restored blob 25d2042 (HEAD 25d2042) git diff HEAD --name-only: [] VERDICT ablation-restored self-test exit=0 ``` Direction predicted before the run and observed: **turns red**. The module is run directly from source by `node scripts/pm/…` — no build and no `dist/` between the edit and the run, so the on-disk proof is the whole preflight.⚠️ **A named gap, not a hidden one**: the battery drives the builder and the renderer, and it cannot see `main`'s **emission**. An ablation that deleted the two lines in `main`'s `finally` would come back green. What covers emission is the three live blocks quoted above, taken on this head across three different exits. ## Candidate cause, unproven — ⛔ not fixed here Two readings taken while wiring the record. Neither is acted on in this PR. **1. On the blob all three 2026-09-13 runs ran, exit 4 was the DETERMINISTIC answer for that pair — so what is unexplained is the two 0s, not the 4.** - The file's last change before those runs was `a5ed18ced` (2026-09-12T06:05:25Z, "a key-INITIAL clause-② line that QUOTES the spelling is not a declaration"); its next change was `4e3a496ba` at 2026-09-13T17:19:18Z, after all three runs. `a5ed18ced`'s blob is `aecbb2d86683eb908468fdacaac2ff53753f06ef` — the same blob PR objectstack-ai#18448's body independently cites as "the exact blob the 2026-09-13 readings were taken from". - The governing claim on card objectstack-ai#17425 at that moment was comment `5650083758` (2026-09-13T01:57:23Z). Its line 3 opens `Clause-②: no —` and then quotes the spelling again inside the same line. Run first-hand against **that historical blob's own `readClause2Line`**: `{"kind":"near-miss","reason":"describing"}`, and `cardDeclaration` on a one-claim thread reads `missing` — ⛔ not a declaration. Today's copy reads it identically. - A `--pair-json` document assembled from the REAL thread as it stood at 03:04:09Z (its 9 comments, both carriers' real label event streams) answers **exit 4, MISPLACED** on this PR's head, quoting the superseded `Clause-②: yes` and naming `5650083758` as the correction target — which is what the 03:04Z reviewer and the 02:53Z dev round both reported. - ⇒ The 4 is reproducible and mechanically explained. The 0s are not. ⭐ Exactly the difference the record's `claim.selected` fingerprint and `claim.clause2-line` would have shown, had the 0-runs printed one. -⚠️ Limits of this reading: the historical module was exercised for `readClause2Line` (self contained) and `cardDeclaration` (which imports today's sibling modules); the commit ordering is read from a shallow checkout, corroborated by objectstack-ai#18448's independent citation of the same blob. **2. The comment read — the one the declaration limb depends on — is the only read here with no page discipline.** `readCardComments` issues ONE request, `/issues/N/comments?per_page=100`, with no `page=` ladder and no short-read check. `readCarrierEvents` and `readPullFiles` both page to exhaustion and answer `null` (UNJUDGED, never clean) when their cap is hit, for the reason their own docblocks state. A card thread past 100 comments therefore loses its tail silently, and the claim pool is built from whatever came back. Not the cause on objectstack-ai#17425 (7 comments at 02:53Z, 16 today), but it is a live fail-open in this reading. The record makes it visible for the first time: request `objectstack-ai#3` prints its row count, so a `(100 row(s))` on a `per_page=100` request is now readable. ⛔ Not fixed here; the seat files or re-scopes. ## Gates Derived with `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` from the worktree with no hand-fed path list; re-derived after rebasing onto current `main` (the derivation was STALE-TREE by 4 commits) — **identical command list**. All 34 run at head `f5773ce08`, exit codes captured redirect-then-`$?`: ```text 0 :: node scripts/check-adr-0087-registration.mjs --base origin/main 0 :: node scripts/check-adr-0087-registration.mjs --self-test 0 :: node scripts/check-changeset-no-major.mjs --base origin/main 0 :: node scripts/check-changeset-no-major.mjs --self-test 0 :: node scripts/check-ci-filter-parity.mjs 0 :: node scripts/check-closing-keyword-parity.mjs 0 :: node scripts/check-closing-keyword-parity.mjs --self-test 0 :: node scripts/check-comment-mask-corpus.mjs 0 :: node scripts/check-declaration-mirrors.mjs 0 :: node scripts/check-declaration-mirrors.mjs --self-test 0 :: node scripts/check-scripts-symbol-anchors.mjs 0 :: node scripts/check-scripts-symbol-anchors.mjs --self-test 0 :: node scripts/check-self-test-wired.mjs 0 :: node scripts/check-self-test-wired.mjs --self-test 0 :: node scripts/check-self-test-workflow-commands.mjs 0 :: node scripts/check-self-test-workflow-commands.mjs --self-test 0 :: node scripts/check-whole-set-label-write.mjs 0 :: node scripts/check-whole-set-label-write.mjs --self-test 0 :: node scripts/pm/bare-root-worklist.mjs --self-test 0 :: pnpm check:agent-test-spelling 0 :: pnpm check:bash32-floor 0 :: pnpm check:changeset-gate-self-tests 0 :: pnpm check:cli-command-ids 0 :: pnpm check:cross-package-test-inputs 0 :: pnpm check:driver-memory-census 0 :: pnpm check:entry-guard 0 :: pnpm check:nul-bytes 0 :: pnpm check:parse-guard 0 :: pnpm check:pm-clause2-carriers 0 :: pnpm check:pnpm-filter-targets 0 :: pnpm check:ratchet-remedy-authority 0 :: pnpm check:refd-timer-probe 0 :: pnpm check:watch-hint-literal 0 :: pnpm check:pm-dispatch-gates ``` Reconciled: `dispatch-gates --ran` ⇒ **34 derived, 34 run, 0 NOT-MEASURED, 0 UNRUN**. Repo-wide `pnpm lint` (`eslint . --no-inline-config`) at `f5773ce08`: **exit 0**. `grep -naP` for control bytes over the changed file: no hits. ⛔ Outside these 34, as the derivation itself prints: 53 artifact-roster families, 11 wide-population families, 7 pending-changeset families, 1 path-scheduled CI job and the always-runs tail. Their absence here is not a clearance. ## Acceptance notes Out of scope, noted and ⛔ not filed: - The read-path report and the input record now also print on the `--pair-json` **usage refusals** (a missing file, a non-JSON document, a board conflict), because everything past the board resolution moved inside one `try`/`finally`. One extra stderr line on those paths, in the direction the file's own header argues for. Carrier: whoever next edits `main`. - `main`'s `--pair` value is parsed in two places now (once for `run.mode`, once for the pair itself). Both read the same argv through `flagIndex`; a reader may prefer one. Carrier: whoever next edits `main`. --- 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01Gqi43smmqjJ5sUrhfoPeKu --- _Generated by [Claude Code](https://claude.ai/code/session_01Gqi43smmqjJ5sUrhfoPeKu)_ Co-authored-by: Claude <noreply@anthropic.com>
akarma-synetal
pushed a commit
to akarma-synetal/framework
that referenced
this pull request
Sep 28, 2026
…ion, rendered as written (objectstack-ai#18730) Fixes objectstack-ai#18543 Clause-②: no `scripts/pm/post-stamped.mjs` only. Non-governed `scripts/pm/**`; `skip-changeset` (nothing in any package's `files[]` moves). ## The defect, in one paragraph The tool substituted the act-clock token **wherever it appeared**, including inside a passage that was *quoting the token itself* — because substitution ran on bytes and a fence was treated as a rendering instruction with no meaning for it. A seat writing about the stamp contract therefore could not quote this tool's own diagnostic through this tool: the quotation came back rewritten, the write succeeded, the read-back reported clean, and the only signal was a substitution **count** that nothing compared to intent. Measured three times inside one hour, by two seats: a false quotation published on objectstack-ai#14251; the same hit in the sentence *describing* it and again in the comment *reporting* that (5707634696); and the dispatch claim for this card refused `[quoted-not-a-stamp]` for spelling the contract's own documentation placeholder (5717851201). The triage seat carries a hand-written workaround in its standing prompt — 「⛔ 描述这个语法时不要写出令牌字面形状,会被当真令牌解析」 — which is what this change retires. ## Before-readings — this tree at `f6189a43f9`, offline, through `--dry-run` All three taken before any edit. Every token below is written in the **new** quoting spelling, so this PR body is itself a demonstration; the inputs are shown as they were typed. **(a) The card's measured instance — the token in an inline code span inside a blockquote.** Input: a blockquote quoting this tool's own status line, with the token inside backticks, plus one live token in a subscript line. ``` post-stamped: DRY RUN — nothing was written. 2 token(s) substituted with `2026-09-17T16:34Z`, 0 quoted stamp(s) rendered verbatim. ... > `post-stamped` reported it faithfully: 「*stamp: none substituted — this body carried no `2026-09-17T16:34Z`, so there is no clock to check*」 ``` ⇒ substituted **inside the quotation**; `substitutions: 2` where the author meant 1. Exit 0. A sentence this tool never printed, published as a quotation of it. **(b1) UNESTABLISHED POINT 1, now measured — the quoted-stamp form inside a quotation.** Input: `> The second spelling is ` + the quoted-stamp token in backticks, plus one live token. ``` post-stamped: DRY RUN — nothing was written. 1 token(s) substituted with `2026-09-17T16:34Z`, 1 quoted stamp(s) rendered verbatim. ... > The second spelling is `2026-09-08T14:00Z` — a reading of something else. ``` ⇒ **worse than (a)**: the braces vanish, so the quotation no longer even shows which route it was about, and the status line's `1 quoted` is byte-identical to what a *legitimate* declared reading prints. There is no count to compare against intent at all. **(b2) UNESTABLISHED POINT 2, now measured — a fenced code block.** Input: the card's own re-check recipe inside a triple-backtick block, plus one live token. ``` post-stamped: DRY RUN — nothing was written. 2 token(s) substituted with `2026-09-17T16:34Z`, 0 quoted stamp(s) rendered verbatim. ... printf 'quoting the token: 2026-09-17T16:34Z\n' | node scripts/pm/post-stamped.mjs --dry-run --comment 18543 ``` ⇒ a fence was **not** a quotation: substituted exactly like prose. So "no substitution inside fenced code" was a real behaviour change to measure, and fencing alone would not have covered (a), which was an inline span. **(c) The claim's refusal.** Input: the quoted-stamp form with an ellipsis payload, inside backticks. ``` post-stamped: REFUSED — 1 stamp-contract problem(s) in the body. Nothing was written. 1. [quoted-not-a-stamp] ... does not declare a stamp. The quoted route renders a reading of something else VERBATIM, so its contents must be one `YYYY-MM-DDThh:mmZ` and nothing else — it is a declaration, not a free-text escape from the contract. ``` Exit 2. The same refusal fires on the documentation placeholder `YYYY-MM-DDThh:mmZ`, which is the spelling **this file's own refusal text uses** — so the tool could not quote its own refusal back through itself. ## The chosen spelling: Markdown code is a quotation > **INSIDE A QUOTED SPAN THIS TOOL RENDERS TEXT, NOT TOKENS.** A quoted span is one of Markdown's two code constructs: a **fenced code block** or a **backtick code span**. Inside one, exactly three rules stand down — substitution, the opener scan, and the quoted-stamp validation. Nothing was added to the token contract: it still has exactly two spellings, and there is still no flag that turns it off. ### The four axes - **实际业务需求.** Measured, not speculative: three live hits inside one hour across two seats, a false quotation standing on a public card until someone re-read it, and a hand-written workaround living in the triage seat's standing prompt for dozens of rounds. All four measurements above are code constructs — an inline span, a fenced block, a backticked declaration — so the structural rule is the one the evidence actually points at. A fence-only rule would have missed (a), the card's own instance. - **项目长远合理性.** Contract-first and no workaround: the quoting rule is Markdown's own, not a second vocabulary this tool invents and then has to document, and the token contract is untouched. The sustainable version of "explain the contract on a card" is that the contract is quotable; the temporary version is every seat carrying its own prompt-level workaround, which is the state being retired. - **防 AI 写代码犯错.** This is the axis that decides it. A structural rule is **visible in the stored artefact**: a reader who has never heard of this tool sees backticks and knows the passage is quoted, in the spelling every other quotation on the board already uses. A magic word is invisible and has to be memorised — and would itself need quoting the first time anybody documented it. The contract also tightens rather than loosens: quoting suppresses what *renders* a token, never what *judges a stamp a human typed*, so no stamp can hide behind backticks, and the one direction that could have weakened a refusal (making the MIXED trigger quote-aware) is deliberately not taken. - **创业阶段不扩散需求.** Zero new surface: no third token, no flag, no option, no config. One predicate (`insideQuotedSpan`) and one span scanner, consumed by the four rules that already existed. No transition window and no double spelling — the old behaviour is gone in the same commit, which is what 「短期不考虑渐进」 asks for. ### Rejected, one line each - **`--expect-now=N` (the card's candidate A).** Not a quotation spelling at all — it refuses the body instead of letting the quotation be written, so the seat still cannot quote the diagnostic; and being opt-in, it protects only the author who already remembered. - **An escape spelling (candidate C).** A third spelling in a contract whose whole value is that it has exactly two, invisible to a reader who does not know the tool, and subject to the same regress the moment somebody has to quote *it*. - **An explicit opt-out flag.** The one thing this file says there is no flag for, and it leaves no trace in the artefact of why a token was not substituted — a reader cannot tell a quotation from a bug. ## What it does NOT suppress — the load-bearing asymmetry Quoting changes what is **rendered**, never what was **authored**. A stamp inside a fence is still digits on the board. | rule | inside a quoted span | why | |:---|:---|:---| | POSITIONAL | reads every line, code included | a bare stamp in a code span on the opening line is the act's own time typed by hand, backticks or not | | MIXED | triggers on the act-clock token appearing **anywhere**, quoted or not | an author who spells the token knows it exists; deliberately NOT made quote-aware, because that is the one direction this change could have weakened a refusal | | MASKING | `maskQuotedStamps` blanks a quoted-stamp token only where it IS a token | otherwise a hand-typed stamp could hide from the bare-stamp scan behind backticks — the accident this rule may never buy | **A bare stamp inside the quoting spelling is REFUSED, exactly as in prose** (pinned, both positions). One body changes direction as a consequence: the quoted-stamp form carrying **real digits** beside a live token used to be accepted and rendered as bare digits — that acceptance *was* defect (b1) — and is now MIXED-refused. Because the old remedy text would then prescribe a route that cannot work inside a quotation, the refusal gained a clause naming the quotation and pointing at the placeholder form; a stamp with even one unquoted occurrence still gets the ordinary remedy, with no such clause. ## The status line no longer leaves a count as the only signal Before, the line carried the substitution count, the token spelling and a quoted count, and stopped there — the card quotes that form. After, the same line carries a third number, and on the same body as before-reading (a) its new tail reads: ``` · verbatim: 1 opener(s) inside a quoted span, left exactly as written ``` So (a) now reports one substituted and one left verbatim, where the filed run could only say `2`. Those two numbers are what (a) had no way to produce. `--json` gains `verbatim`, and the `DRY RUN` line spells the same three counts through one renderer (`substitutionSummary`), so the three outputs cannot describe one render three ways. ## After-readings — same four inputs, same tree, after the change - (a) exit 0 — one substituted, zero quoted, `verbatim: 1`; the quotation comes back byte-identical and the act's own token, outside it, still gets the clock. - (b1) exit 2 — MIXED, with the quoted-span clause. The argued outcome: real digits inside a quotation are digits on the board. - (b2) exit 0 — one substituted, zero quoted, `verbatim: 1`; the fenced recipe survives intact. - (c) exit 0 — the ellipsis payload and the documentation placeholder are both **quoted verbatim, not refused**. The dispatch claim for this card would now pass byte-for-byte, which is the retirement of the triage seat's standing workaround. ## Pins New battery, `the quoting spelling: Markdown code is a quotation, and a quotation is rendered as written`, 51 cases, registered in `SELF_TEST_BATTERIES` with the battery floor raised 11 → 12. It covers, in order: the card's blockquote-inline-span instance rendering verbatim **and** its before-reading control (the same body with the backticks removed is still substituted, 2); the fenced block, a tilde fence, an info string, a backtick-carrying info string (not a fence), an unclosed fence, a fence inside a blockquote, the blockquote ending it, and a four-space indented block being explicitly **not** the spelling; the claim's ellipsis refusal now rendering verbatim with its prose control still refused; the quoted-stamp form inside the spelling (rendered as the token, counted verbatim, not as a quoted stamp) with its prose control still rendering from the declaration; an unknown token name and an unclosed opener inside a quotation being text, both still refused in prose; the bare-stamp asymmetry in both positions, the mask rule, the quote-unaware MIXED trigger, and the remedy clause with its one-unquoted-occurrence control; the code-span mechanics (unmatched run, double run, one-line-only search, two spans on a line, a backtick run inside a fence); and the prose control asserting byte-identical output with `verbatim: 0`. One pre-existing case was flipped rather than added — the opener-scan battery's `⛔ …and a token inside backticks is STILL substituted — a fence is not an escape`, which is precisely the sentence this card retires. **Every other existing case is untouched and green**: the first run after the implementation, before any new case existed, reported `1 of 257 case(s) failed` — that one — so the 256 others are byte-identical in behaviour. Self-test line: **257 cases across 11 batteries → 308 cases across 12 batteries.** ## Ablation Committed the fix first, then reverted the single predicate that recognises the quoting spelling — `insideQuotedSpan` made to answer `false` always, one line, with every other line of the change left in place. The mutation was proved on disk by occurrence count before the run and the restore proved by hash against the HEAD blob, under a `trap … EXIT INT TERM` holding an absolute path. The result is recorded below with both legs' numbers. (No build step is involved: this file is run directly by `node`, resolves through no package `exports`, and has no `dist/`, so `ablation-dist-preflight.mjs` has nothing to preflight.) | leg | worktree blob | on-disk proof | self-test | |:---|:---|:---|:---| | before (= `HEAD` blob) | `baba105b5625fc16` | predicate body present ×1, marker ×0 | 308 pass / 12 batteries, **exit 0** | | ABLATED — `insideQuotedSpan` answers `false` | `62cb814417c1fd1a` | predicate body ×0, marker ×1 | **24 of 308 failed**, exit 1 | | restored — `git checkout HEAD -- ABSOLUTE_PATH` | `baba105b5625fc16` (identical to the `HEAD` blob) | marker ×0, `git diff HEAD` 0 lines | 308 pass / 12 batteries, **exit 0** | **Exactly the new cases red, the old ones green.** All 24 belong to this change: 23 of the new battery's 51, plus the one flipped opener-scan case. **Zero pre-existing cases went red.** The other 28 new-battery cases are CONTROLS — prose behaviour, the before-reading controls, the prose-still-refused halves — and stay green under the ablation by design, which is what a control is for. One failure detail is worth reading on its own: the counts case fails with `sub=2 verb=0`, which is byte-for-byte the filed reading `substitutions: 2` from before-reading (a). The ablation does not merely turn the pins red; it reproduces the card's measured defect exactly. ## Gates Derived from this worktree with `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` (no hand-fed path list; change set = 1 path, `scripts/pm/post-stamped.mjs`, three-dot against merge base `f6189a43f`). Every derived command run, exit code captured with redirect-then-`$?`, then reconciled with `--ran`. **29 derived, 29 run, every one 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-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-scripts-symbol-anchors.mjs` :: **exit 0** - `node scripts/check-scripts-symbol-anchors.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-whole-set-label-write.mjs` :: **exit 0** - `node scripts/check-whole-set-label-write.mjs --self-test` :: **exit 0** - `node scripts/pm/bare-root-worklist.mjs --self-test` :: **exit 0** - `pnpm check:agent-test-spelling` :: **exit 0** - `pnpm check:bash32-floor` :: **exit 0** - `pnpm check:cli-command-ids` :: **exit 0** - `pnpm check:cross-package-test-inputs` :: **exit 0** - `pnpm check:driver-memory-census` :: **exit 0** - `pnpm check:entry-guard` :: **exit 0** - `pnpm check:nul-bytes` :: **exit 0** - `pnpm check:parse-guard` :: **exit 0** - `pnpm check:pm-dispatch-gates` :: **exit 0** - `pnpm check:pm-post-stamped` :: **exit 0** - `pnpm check:pnpm-filter-targets` :: **exit 0** - `pnpm check:ratchet-remedy-authority` :: **exit 0** - `pnpm check:refd-timer-probe` :: **exit 0** - `pnpm check:watch-hint-literal` :: **exit 0** Reconciled: `dispatch-gates --ran` ⇒ `29 derived famil(ies) accounted for — 29 run, 0 NOT-MEASURED (a DERIVED zero — all 29 recorded an exit code and none of them is 3)`, **exit 0**. Repo-wide, as PR objectstack-ai#18654 did: `pnpm lint` (`eslint . --no-inline-config`) **exit 0**, run at `d7dffeb045`, which is this branch's final commit — no commit has landed on the branch since.⚠️ NOT MEASURED, declared rather than claimed: the 53 artifact-roster families, the 11 declared wide-population families, the 14 pending-changeset families, the 2 families taking a value from the workflow and the 1 path-scheduled CI job are each outside the derived 29 and are CI's. `dispatch-gates` prints every one of them under its own heading; none is read here as a clearance. ## This body passes the tool it is fixing `node scripts/pm/post-stamped.mjs --dry-run --comment=18543 --file=THIS_BODY` ⇒ **exit 0**. Its first draft did not: it quoted the act-clock token literally *and* carried the concrete stamps from the before-readings, and the MIXED refusal fired twice — correctly, and with the new quoted-span clause naming the quotation each time. That shape is the measured cost of keeping the MIXED trigger quote-unaware, and it is the rule holding rather than a gap: every quotation above is verbatim, and what changed to clear the refusal was spelling the token in words outside the quoted evidence, not editing the evidence. ## Not in scope objectstack-ai#18693 (the header's unmeasured issue-body footer cell) and objectstack-ai#18709 (exit 4 on the seat's own footer re-anchor) are separate cards queued on this file behind this one; both were read and neither is addressed here. `classifyReadBack`, `footerReAnchoring` and `sentBodyLanded` are untouched, and nothing about what is SENT changed except the rendering of quoted tokens. No retry logic, nothing under `.claude/**`. --- _Generated by [Claude Code](https://claude.ai/code)_ Co-authored-by: Claude <noreply@anthropic.com>
akarma-synetal
pushed a commit
to akarma-synetal/framework
that referenced
this pull request
Sep 28, 2026
…nd one constant holds both tools to it (objectstack-ai#18738) Fixes objectstack-ai#18701 Clause-②: no ## The defect `check-clause2-carriers.mjs --template` prints, and has always printed, that a review of record is `ONE comment on the PR or its card`; C6/C7 judge it on both threads through `locateReviewOfRecord`; the governed text says the same in four places. The Governed Surface Queue Guard's REFERENCES tier read **the pull request's thread alone**. The two tools already shared every recogniser — the heading marker, the head-sha span test, the newest-of resolution, the `Reviewed-by:` key line, the `Served-tier:` reader — and the one thing they did not share was the **set of threads** to run them over, which is the only fact a spelled-out copy of the filter could not copy. Measured cost, on one pull request in one afternoon: the skills seat posted its `## Contract review` on carrier card objectstack-ai#18426 (comment `5716694216`, 15:10:43Z), `--pair 18689` read it as C6-RECORD on the `card thread`, the `merge_group` leg (run `35238999808`) refused with `0 comment(s) read on the PR thread`, PR objectstack-ai#18689 was dequeued `CI_FAILURE` at 15:36:18Z, and only a **second copy** of the same comment on the PR thread (`5717108355`) cured it. ## The canonical sentence — the governed text is not silent Read on `origin/main` (`080688b561`, the branch base). Quoted verbatim and untranslated, because rewriting a quoted ruling rewrites the ruling: - `.claude/skills/pm-dispatch/references/contract-review.md` **:27** — 「复核记录 = 一条评论落 PR 或卡,达档与默认档同形;散文、dev 自评、`os-dev-report` 恒不算。」 - same file **:40** — 「落地前检三条:① 席内条款②复核 PASS 在案,即 PR 或卡上同形的复核记录(档位按实测)。」 - same file **:59** — 「PASS、FAIL 与作废都落 PR 或卡;……」 - `.claude/skills/pm-dispatch/SKILL.md` 〈入队与落地〉 **:640** — 「交付后复核归派发席:达档席内审,未达档循保险丝起子代理;记录 = 同形评论落 PR 或卡。」 (The claim comment's aside that `references/contract-review.md` carries no such sentence is the one premise this card's dispatch got wrong; :27 is that sentence, and it is on `main`. Nothing else in the premise moved — the guard, the template, the docblocks and the objectstack-ai#18689 evidence all read as the card describes them.) **Which tool disagrees with it: `scripts/pm/check-governed-queue-guard.mjs`.** So the GUARD moves, all of it. No governed file is touched by this PR. ## Why the guard and not the template — the four axes - **实际业务需求.** The record on the card is not a hypothesis: it is what a seat did, on 2026-09-17, following this repo's own `--template`. Both carriers are live on every pair — the clause-② declaration is hung on both, `--pair` judges both, the carrier label is stripped from both — and `check-clause2-carriers` has read both threads since objectstack-ai#17302. The population that would be served by moving the template instead is empty: nobody is asking for the card to stop counting. - **项目长远合理性.** Contract-first. The governed text is the contract and the two tools are its consumers; a consumer reading a narrower set than the contract declares is the defect, and the repair belongs at the consumer, not at the contract. 基本裁决原则 puts 声明 above 实现 above 文档面 — moving the template would be narrowing the declaration to match an implementation gap, which is the one direction that rule forbids. - **防 AI 写代码犯错.** This is a declared-but-not-honoured capability in its purest form: an instruction tells an author where to put an artifact, and a gate silently does not look there. The structural repair is not "say the same thing in more places" — it is ONE exported constant that the instruction and the reader both derive from, plus a pin that reds if either is edited alone. Moving the template would leave three more hand-written location sentences (the guard's option-3 text and two docblocks) free to drift the same way one layer along. - **创业阶段不扩散需求.** Moving the guard is the SMALLER change and adds no capability: the helper already exists, the card number is read off the pull object the head read already fetches (zero extra requests for it), and at most one extra thread read is bought — only for a references-tier pull request with no authorized approval, a population refused outright today, so nothing that passes now pays anything. Moving the template would be a RETIREMENT of a carrier the protocol uses everywhere else, across governed files, which is a maintainer's ruling and not a dev PR. No axis conflicts with another. A fifth, decisive fact: the template side cannot move from here at all — its sentences live in `.claude/skills/pm-dispatch/**`, off this card's file surface and governed. ## The shared constant, and one derivation of "the record on this head" `check-clause2-carriers.mjs` — the file that owns the reader — now declares the thread set once: ```js export const REVIEW_OF_RECORD_THREADS = Object.freeze([ Object.freeze({ where: 'PR', rows: 'prComments', number: 'pr', words: 'the PR' }), Object.freeze({ where: 'card', rows: 'cardComments', number: 'card', words: 'its card' }), ]); export const REVIEW_OF_RECORD_LOCATION = REVIEW_OF_RECORD_THREADS.map((thread) => thread.words).join(' or '); ``` Three consumers, all derived from it and none spelling a set of its own: 1. `locateReviewOfRecord` builds the rows it searches, its gap sentences and its per-thread read counts from the list. `--template`'s and `--pair`'s output is **byte-identical** to `main`'s. 2. `contractReviewTemplateLines` builds its printed sentence from `REVIEW_OF_RECORD_LOCATION`, so an instruction can never offer a thread no reader searches. 3. `check-governed-queue-guard.mjs` fetches **one thread per entry in the loaded list** — and its whole record reading is now `locateReviewOfRecord`, imported through the existing lazy loader rather than re-spelled. The guard's own two-line filter is gone; what is left there is the tier gate on top of the located record (`Served-tier:` stands, and the objectstack-ai#18060 model-identifier refusal), which is that leg's own fact. The card the record may live on is **derived, never configured**: `deliveredCardNumber(pull)` enumerates the numbers the body or branch could be naming and asks `deliveryEvidence` — the one relation `derivePairs`, H8 and H31 already pair a PR with its card by — so it can never accept a card that relation rejects. A pull request naming no card, or two cards at equal strength, searches NO card thread and the refusal says so: the refusing direction, the only one a governed reading may be wrong in. The card's number and the head sha come from ONE `GET /repos/{owner}/{repo}/pulls/{number}` — the request the head read already made — so the card thread costs no extra call for the number, and the guard keeps its zero-write, event-payload-only posture with no checkout. `REVIEW_OF_RECORD_LOCATION` is mirrored at the guard's module scope for the renderer only (`renderGuardVerdict` is pure and synchronous, and a module-scope import of the sibling is the measured exit-13 deadlock its own docblock records). The SET the leg acts on is never a mirror — it is the loaded list — and the mirror's words are pinned to the constant in the guard's battery. ## The pin `check-clause2-carriers.mjs` owns the reader, so the pin lives in its self-test — and it is a **measurement**, not two lists written side by side: - **STATED** — the locations read out of the text `contractReviewTemplateLines()` actually prints. - **READ** — obtained by **driving `check-governed-queue-guard.mjs`'s `runGuard`** once per entry in `REVIEW_OF_RECORD_THREADS`, with a record built from this file's own `contractReviewRecordLines()` sitting on that thread **alone**, and collecting the threads whose run answers `exitCode === EXIT_CLEAR` with `record.state === 'stands'`. The case asserts `stated === read === the constant's own set`, plus its own controls: the set has two locations (not vacuous); each run located the record on the thread it was posted to; the guard fetched exactly one thread per entry, no more and no fewer; and a run with the record on NEITHER thread is refused, so the pin can fail. `selfTest()` became `async` to take that lazy import — the dispatch uses `.then(...)`, never a top-level `await`. ## Counterfactual — the objectstack-ai#18689 dequeue, offline One fixture, driven through `runGuard` with no network and no git: a references-tier pair whose record sits only on the card thread. The same script runs against either generation (it passes both the old `fetchPullHead` and the new `fetchPull`). | | exitCode | conclusion | `record.state` | `record.where` | |---|---|---|---|---| | **before** (`080688b561`) | `3` REFUSED | `refused` | `absent` | — | | **after** | `0` | `cleared` | `stands` | `card` | Before, rendered: `⛔ NO review of record on this head either (1 comment(s) read on the PR thread)` — the objectstack-ai#18689 sentence, reproduced. After, rendered: `✅ review of record on this head: comment 5716694216 (2026-09-16T15:10:43Z) on the card thread is a ## Contract review comment naming ...` ## Ablation Two legs, each: on-disk proof of the mutation (`grep -c` of the removed text and of the injected text, plus the `git hash-object` blob before and after), then the self-tests, then restore, then a byte-identity check against the HEAD blob. Both scripts carry `trap restore EXIT INT TERM` with an absolute path. No build or `dist/` is involved — these two files are run directly by node and by `package.json`, so the on-disk proof is the whole preflight. **Leg A — the loop that unifies the thread set** (`for (const thread of recognisers.threads)` narrowed to the first entry). Blob `9a111f6c` → `b57227d0`; removed-text count 1 → 0, injected-text count 0 → 1. Cross-tool pin: **4 of 802 failed**. Guard battery: **25 of 253 failed** — the loop is load-bearing for the whole references leg, not only for the card. **Leg B — the line that supplies the card's number** (`card: card.card` → `card: null`), which reproduces the pre-objectstack-ai#18701 thread set exactly. Blob `9a111f6c` → `9d935d92`; counts 1 → 0 and 0 → 1. This is the clean reading the card asks for: - **new pin RED**: `check-clause2-carriers` **3 of 802 failed**, all three the new battery — `stated=[PR,card] read=[PR]`, the card run locating nothing, and the fetch list `[[4101],[4101]]` instead of `[[4101,4102],[4101,4102]]`. - **new guard cases RED**: `check-governed-queue-guard` **5 of 253 failed**, all five in the `objectstack-ai#18701` battery (record on the card alone, both threads fetched, the rendering naming the thread, newest-governs across threads, the branch-name fallback). - **old cases GREEN**: 799 of 802 and 248 of 253 — every pre-existing battery in both files, `objectstack-ai#18020` included, passes under the ablation. Restore, both legs: worktree blob `9a111f6c…` equals the HEAD blob, `git diff HEAD` empty, `git status --porcelain` empty, and both self-tests back to exit 0. ## Self-tests | family | before | after | |---|---|---| | `node scripts/pm/check-governed-queue-guard.mjs --self-test` | 239 cases, 21 batteries declared | **253 cases**, 22 batteries — new: `⭐ objectstack-ai#18701: the record lives on the PR or its card, and BOTH are read` (14, floor 14); `SELF_TEST_BATTERY_FLOOR` 20 → 21 | | `pnpm check:pm-clause2-carriers` | 786 cases, 28 batteries declared | **802 cases**, 29 batteries — new: `objectstack-ai#18701: ONE thread set -- what the template STATES is what the queue guard READS` (16, floor 16); `SELF_TEST_BATTERY_FLOOR` 27 → 28 | Both exit 0. `--template`, `--help` and the no-event refusal path (exit 1, its own message) all still answer. ## Gates `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` derived from the worktree with no hand-fed path list (change set: the two scripts, 2 committed / 0 working tree / 0 untracked, three-dot against merge base `080688b56`). **36 derived, 36 run, every one exit 0**, reconciled with `--ran` after a fresh `git fetch origin main` — `✓ dispatch-gates --ran: 36 derived famil(ies) accounted for — 36 run, 0 NOT-MEASURED (a DERIVED zero — all 36 recorded an exit code and none of them is 3)`. ```text 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-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-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-scripts-symbol-anchors.mjs :: exit 0 node scripts/check-scripts-symbol-anchors.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-whole-set-label-write.mjs :: exit 0 node scripts/check-whole-set-label-write.mjs --self-test :: exit 0 node scripts/pm/bare-root-worklist.mjs --self-test :: exit 0 node scripts/pm/check-governed-queue-guard.mjs --self-test :: 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 pnpm check:closing-target-claim :: exit 0 pnpm check:cross-package-test-inputs :: exit 0 pnpm check:driver-memory-census :: exit 0 pnpm check:entry-guard :: exit 0 pnpm check:nul-bytes :: exit 0 pnpm check:parse-guard :: exit 0 pnpm check:pm-clause2-carriers :: exit 0 pnpm check:pm-dispatch-gates :: exit 0 pnpm check:pnpm-filter-targets :: exit 0 pnpm check:ratchet-remedy-authority :: exit 0 pnpm check:refd-timer-probe :: exit 0 pnpm check:watch-hint-literal :: exit 0 ``` Repo-wide `pnpm lint` (`eslint . --no-inline-config`, the whole tree, as PR objectstack-ai#18654 did) :: **exit 0**, no output. Every exit code above was captured with redirect-then-`$?`, never across a pipe. `origin/main` moved to `62bce5c297` while this ran; its three commits touch `packages/**` and `.changeset/**` only — nothing under `scripts/**` and nothing this diff reads — so no merge was taken and the derivation above is unaffected. ## Scope Two files, `scripts/pm/check-governed-queue-guard.mjs` and `scripts/pm/check-clause2-carriers.mjs`. No governed file. `skip-changeset`: nothing published moves — `scripts/pm/**` is not in any package's `files[]`.⚠️ **The card read this PR adds is built on `readCardComments`'s paged sibling, not on the un-paged path objectstack-ai#18683 names.** The guard's own `makeCommentReader` is paginated (100 per page, 10 pages, and it THROWS rather than truncating), and it is what serves both threads here — `GET /repos/{owner}/{repo}/issues/{number}/comments` is the same route for a pull request and for a card. objectstack-ai#18683 remains exactly as it was, on `check-clause2-carriers`' own reader, untouched by this PR. Not in scope, as dispatched: `check-clause2-carriers`' un-paged `readCardComments` (objectstack-ai#18683 — read, not fixed); objectstack-ai#18536's machine-side lane/tier re-keying; the REFERENCES-tier rule itself (which paths qualify), untouched. ## Acceptance notes - `deliveryEvidence`'s ranking had no exported ordering, so `DELIVERY_EVIDENCE_PRECEDENCE` is declared in `check-clause2-carriers.mjs` as a MIRROR and measured against that function pair by pair in the self-test rather than trusted. If `check-half-states.mjs` ever wants to own it, that is a one-line move; noted, not filed — no PR or person reaches for a ranking that today has exactly one caller. - The guard no longer loads `check-half-states.mjs` directly: every recogniser it reads now arrives through the file that composes them, and a rename upstream stops THAT module loading, which the leg already turns into a fail-closed refusal with the loader's own reason. `RECOGNISER_SOURCES` is down to one entry and the self-test pins that. Keeping the old direct load would have been a check on something this file no longer reads. --- _Generated by [Claude Code](https://claude.ai/code/session_01Gqi43smmqjJ5sUrhfoPeKu)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
akarma-synetal
pushed a commit
to akarma-synetal/framework
that referenced
this pull request
Sep 28, 2026
… make a near miss audible (objectstack-ai#18756) Fixes objectstack-ai#18680 Clause-②: no ## The defect `CLAIM_COMMENT_MARKER` and `RELEASE_COMMENT_MARKER` anchor the bare word at line start and tolerate only a leading blockquote, so a line written in the decorated spelling a seat uses when it bolds the directive — `**Release:** …` — begins with an asterisk and the record reads as ABSENT. Nothing goes red; the ownership rows (H2, H47, H66, H67) simply read a different history than the thread carries, which is the silent direction. objectstack-ai#10102 made exactly this judgement for the other directive family (`Blocked-by:` / `Restart-when:`) and never for these two markers; PR objectstack-ai#18678 landed H67 declaring the loss on its own row rather than widening, because widening moves three landed rows' populations and is its own card. This is that card. ## Before-readings (reproduced offline, on the fetched specimen) The live specimen is objectstack#16529 comment **5691473966** (os-try-charles, 2026-09-16T03:11:44Z), fetched through the REST proxy and never retyped.⚠️ One correction to the card's prose: the `**Release:**` line is the **45th** line of that comment, not its first — both markers are `m`-flagged and scan every line, so line position was never what hid the record. The asterisks were. | probe | `RELEASE_COMMENT_MARKER` (bare) | |:--|:--| | the specimen's whole comment body | `false` | | the specimen's release line alone | `false` | | the SAME line with `**Release:**` replaced by `Release:` (control) | `true` | | `**Claim:** …` / `` `Release:` … `` / `__Release:__ …` / `## Release: …` / `- Release: …` | all `false` | `undecorateProseLine` — the objectstack-ai#10102 function — was measured rather than assumed: its body is a single `replace` over a character class holding exactly a backtick and an asterisk. It removes backticks and asterisks and **nothing else** — not underscores, not a heading hash. That measurement is what decides which spellings this repair reaches and which become near misses. Consumers of the two exported constants, all found by grep across `scripts/`: nine call sites inside `check-half-states.mjs` itself (H2, `governingClaim`, `latestClaimComment`, H34's guard, `h44ArtefactShape`, `h46ClaimNamesBranch`, `latestMarkedComment` — which is H47's, H49's, H50's, H53's and H67's shared resolver — `SEAT_SIGNATURE_FORMS`, and `h66ReleaseVerdict`), each fed a raw comment body or one raw line; and one cross-file importer, `scripts/pm/check-clause2-carriers.mjs`, which feeds `CLAIM_COMMENT_MARKER` a raw comment body. That importer is **outside this card's file surface and is deliberately unaffected**: the constants keep their bare semantics. ## The ONE place decoration is handled `markerMatches(marker, text)`, declared beside the two markers. It tries the bare reading FIRST and short-circuits, then re-tests against the same `undecorateProseLine` the `Blocked-by:` family uses. All nine in-file call sites now go through it; ⛔ no regex was widened, ⛔ no second stripper exists, and the two constants still describe the bare directive (the cases that pin them still assert on them directly, and they stay green). Two properties fall out: - **Strictly additive, by construction rather than by inspection.** The bare test short-circuits, so ⛔ no body that read before can stop reading. Measured on 770 live comments below: 0 regressions. - **It refuses to undecorate through a markdown LIST ITEM.** The shared stripper takes every asterisk, so a `* Claim:` bullet would become a directive while H20's pinned `- Claim:` stays refused. A list marker is followed by whitespace and a decoration is not; that is the whole discriminator, and it is pinned both ways. ## The near-miss vocabulary — a line that looks like a marker makes a sound Widening alone leaves the same silence one decoration further out, which is the triage's second half (comment 5716952460). `OWNERSHIP_MARKER_NEAR_MISS_FORMS` is a frozen, named roster in the register of objectstack-ai#18560's `SCHEMA_PROPERTY_FORMS`; each member carries its own `example` fixture, and the roster is asserted EQUAL to a frozen list of ids, so a form added without a fixture reds and a form silently dropped reds. | id | what | example | |:--|:--|:--| | `heading` | the directive written as a markdown heading | `## Release: …` | | `list-item` | the directive written as a markdown list item | `- Release: …` | | `underscore-emphasis` | emphasised with underscores, which the shared stripper does not remove | `__Release:__ …` | | `inflected-word` | a spelling the marker's vocabulary does not carry | `Released: …` | | `separator` | the canonical word with a separator that is not the canonical colon | `Release — …` | `ownershipMarkerNearMisses(commentRows)` is the reader; it buys nothing (the sweep hands it threads other rows already paid for), files no finding and proposes no state. It reports on an **unconditional summary clause** (`Ownership-marker near misses: …`) naming the card, the comment id and the offending prefix, capped at five named entries with the remainder counted. A line the reading DOES read is ⛔ never a near miss — the two are complements by construction, so a future widening shrinks this census automatically. H67's declared loss is retired in the same edit, on its row and in its summary clause: both said the decorated line was invisible, and that is no longer true. ## Pins New battery **`H2/H47/H66 decorated ownership marker`**, 98 cases, pinned at 94. The roster floor rose 4 to 5. - the fetched objectstack-ai#16529 specimen: refused by the bare marker, READ through `markerMatches`; the derived bare spelling of the same line matches both ways (control); a release is still not a claim; the record reads from any line of the body - the rows: H2 goes clean on a decorated claim (with the no-claim control still firing); `latestMarkedComment` locates a decorated release; H66 reads it on the canonical leg with destination `pm:queue`, quoted undecorated - what the stripper measures: backticked and bold-italic directives read; `__Release:__` does NOT, and is a near miss instead - the firing controls of a widening, inside the same battery: seven bare spellings still read, prose containing the word still does not, `Released:` is still MALFORMED, the fullwidth colon still does not match (the 2026-08-11 ruling is untouched), a dash-written claim is still H34's row - the bullet guard: an asterisk bullet, a hyphen bullet, an ordered `1.` marker and a blockquoted bullet all refused; the whitespace discriminator pinned as a pair - the vocabulary: every member driven against its own fixture — not read by either marker, reports naming its own form, with the comment id and the offending prefix — plus the counterfactual roster-equality pin, frozen-ness, distinct ids, and ⛔ no `g` and ⛔ no `m` flag - the summary clause: counts, named entries, the cap clause, unconditional rendering, render order, the forwarding contract, and ⛔ no `undefined` - **the PR objectstack-ai#18678 pin, EDITED and not deleted**: `H67⚠️ loss: a DECORATED **Release:** line does not stand the row down` becomes `H67⚠️ loss CLOSED: … now STANDS THE ROW DOWN, as the bare one always did`, and its companion flips from "the row DECLARES that blind spot" to "the row no longer DECLARES a blind spot it no longer has". Its two control cases are untouched and still green, because they assert on the marker CONSTANT — which this PR does not change. Self-test: **4782 cases / 4 batteries becomes 4881 cases / 5 batteries** (98 battery cases, plus one case the per-anchor summary-clause coverage loop registers for the new clause automatically). ## Ablation Revert the one call that routes the markers through the undecorated line (delete the undecorated leg of `markerMatches`, leaving the bare test alone), from the committed state, with an EXIT/INT/TERM trap restoring by absolute path. - on-disk proof before reading any result: injected marker present 1 time, deleted text `const undecorated = raw` present 0 times, HEAD blob `44aed794…` vs mutated blob `b4ab48a2…` (different, so ⛔ not a no-op) - result: **17 red, 4864 green**. Every red is a case about the new reading — the four objectstack-ai#16529 cases, H2/H47/H66 on a decorated record, the five decoration cases, the two bullet-discriminator cases, and the flipped H67 pin. Every bare-spelling control, every near-miss vocabulary case, the `Released:` / fullwidth / prose negatives and the marker-constant pins stayed green. - restore verified by hash (`44aed794…` again) AND by an empty `git diff HEAD` — ⛔ not by an exit code ## Live-board delta — report-only, ⛔ no state write of any kind Two full sweeps, `node scripts/pm/check-half-states.mjs` against `objectstack-ai/objectstack`: BEFORE on a detached worktree at `62bce5c29` (17:53Z to 18:01Z), AFTER on this branch (18:01Z to 18:09Z). **H2 / H47 / H66 verdicts: identical.** H2 fired on objectstack-ai#13597 and objectstack-ai#15638 in both; H47 and H66 listed nothing in either. Eight rows differ between the two runs (H14 objectstack-ai#18617, H38 objectstack-ai#7623, H52 objectstack-ai#18617 dropped; H1 objectstack-ai#18709, H19 objectstack-ai#18734, H36 objectstack-ai#18414/objectstack-ai#18720/objectstack-ai#18741 appeared) and every one is board churn in the eight minutes between them — none reads an ownership marker. **objectstack-ai#16529 specifically** still lists on H67 in both, and the reason has nothing to do with the marker: its newest merge is now PR objectstack-ai#18678 (merged 2026-09-17), which is NEWER than the 2026-09-16 release record, so "nobody has looked since the delivery landed" is a correct reading. Its row text did change — the declared loss is gone. Because a sweep only judges threads it bought, the zero above understates the reading. So the same question was asked directly, over the 286 open `pm:queue` / `pm:dispatched` cards and their 770 comments: - **NEWLY READ as an ownership record: 4, on 4 cards; REGRESSIONS: 0.** - objectstack-ai#16529 comment 5691473966 — `**Release:** …` (the card's own specimen) - objectstack-ai#17852 comment 5700605769 — a backticked `Release:` - objectstack-ai#15468 comment 5549954050 — `**Claim:** …` - #14026 comment 5486688759 — a backticked `Claim:` - **Near misses over the same population: 2** — objectstack-ai#16233 comment 5704218834 and objectstack-ai#18143 comment 5707808263, both `### Release:` (form `heading`). Over the sweep's wider 187-thread corpus the clause named three: objectstack-ai#18336 comment 5693290763 (`list-item`), objectstack-ai#15768 comment 5556889538 (`heading`), objectstack-ai#6736 comment 5235658231 (`separator`). And the counterfactual the rows themselves cannot show, offering the SAME live thread to both readings — **3 of 6 cards change**: | card | H47 | H66 | |:--|:--|:--| | objectstack-ai#16529 | FIRES becomes **clean** (the release now answers the claim) | none becomes **`pm:queue`** | | objectstack-ai#17852 | FIRES becomes **clean** | none becomes **the maintainer** | | objectstack-ai#15468 | clean becomes **FIRES** (a bolded claim nobody has answered) | unchanged | | #14026, objectstack-ai#16233, objectstack-ai#18143 | unchanged | unchanged | ⛔ Nothing was written to any card, PR or label from either sweep, and ⛔ no verdict here is a proposal about any of those cards. ## Gates Derived from this worktree with `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` (⛔ no hand-fed path list; change set: `scripts/pm/check-half-states.mjs`, one path). 38 families, every one run, exit code captured by redirect-then-`$?` before any pipe, reconciled with `--ran`. **37 of 38 exited 0**, with the two qualifications named below. Notably: `check:pm-half-states` 0 · `check:nul-bytes` 0 · `check:closing-target-claim` 0 · `check:commit-card-trailers` 0 · `check:self-test-wired` 0 · `check:scripts-symbol-anchors` 0 · `check:declaration-mirrors` 0 · `check:whole-set-label-write` 0 · `check:changeset-no-major` 0 · `check:pm-governed-queue-guard` 0 · `check:cross-package-test-inputs` 0 · `check:parse-guard` 0.⚠️ `pnpm check:nul-bytes` exited **1 on the first pass** and was right to: an editing tool had materialised a backslash-u-0001 escape (written out in words here for the same reason) into a real 0x01 byte in the near-miss dedupe key — the exact slip that gate exists for. Fixed by writing the escape text (byte-identical at runtime), re-run green, and the rule's own `grep -naP` control-byte self-scan over the file returns nothing.⚠️ `pnpm check:pm-dispatch-gates` is the one family whose self-test runs longer than this container's foreground ceiling: a first attempt reached 1768 green cases and was killed by the timeout wrapper at 560s (exit 124 = no verdict reached, which is NOT MEASURED and ⛔ not a red). It was re-run detached; its verdict is reported in this card's `os-dev-report` comment rather than guessed here. ⛔ Its diff-relevant half is unaffected either way — this PR touches neither `dispatch-gates.mjs` nor its fixtures. Repo-wide `pnpm lint` (`eslint . --no-inline-config`): **exit 0**, as PR objectstack-ai#18654 did. ## Not in scope, read and left alone - **objectstack-ai#18664** (`ISSUE_BODY_LIMIT`, queued behind this card on this file) — read, ⛔ not touched. - **H67's own row logic** beyond the pin flip and the two sentences that declared a loss which no longer exists. - **The objectui copy of this file** — byte-pinned, re-synced only through objectui#9395, ⛔ never hand-mirrored. - **How a verdict is written to a card** — unchanged; this file still writes nothing. ## Acceptance notes - H66's summary clause still carries the dated reading 「Measured 2026-09-16 over 29 threads on two boards, the canonical `Release:` line appeared ZERO times」. It names its date and its boards, so it stays true as written, but it was taken with the bare reader and this PR changes what a re-measure would find. Noted, not filed — the sentence is a dated measurement, not a live claim. Who would meet it: the next author of H66's buy-order or clause. - `check-clause2-carriers.mjs` reads `CLAIM_COMMENT_MARKER` against a raw comment body and therefore still cannot see a decorated claim. That is correct for this card's file surface (the constant is unchanged) and is a reading about that file, not a defect in this one. Noted, not filed; the seat decides whether that gate wants the same reading. Who would meet it: whoever next touches that gate's claim leg. --- _Generated by [Claude Code](https://claude.ai/code/session_01Gqi43smmqjJ5sUrhfoPeKu)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
akarma-synetal
pushed a commit
to akarma-synetal/framework
that referenced
this pull request
Sep 28, 2026
…ipts/ablation-replace.mjs beside its sibling preflight (objectstack-ai#18725) Fixes objectstack-ai#18599 Clause-②: no Governed surface (`.claude/**`): draft, `skip-changeset`, an authorized approval lands it and the `domain:skills` seat lands it from there. File surface: `.claude/agents/os-dev.md` + `packages/qa/dogfood/README.md` only. Head `fa55c9d0ad`, base `f6189a43f9` (`origin/main` at worktree creation; the card's readings were taken on `ab1d35925` and re-read here on the tip). Line-neutral on the governed file: **403 / 403**, widest line 120 B before and after, zero lines over the ratchet's 120-byte cap. ## The defect PR objectstack-ai#18598 (card objectstack-ai#18226) landed `scripts/ablation-replace.mjs` — the on-disk mutation step of an ablation: replace through an anchor that must hit, with the write and the restore verified against the disk rather than an exit code, in place of the `sed -i` / `perl -i` family that silently failed in two recorded ablations (2026-08-20, a `-0` run whose `$/` interpolated into a NUL byte; 2026-09-14, an argument swallowed and nothing written, exit 0). No instruction surface pointed at it, while the two that route an agent to its sibling `scripts/ablation-dist-preflight.mjs` do exist — so the helper could only be found by grepping `scripts/`, which is the memory-dependent shape objectstack-ai#18226 was filed against. This PR adds the pointer to exactly those two surfaces, in the order an ablation runs the two tools: mutate on disk through the replace helper, then prove the mutation reached `dist/` with the preflight, then read the run. Adoption-style only: not a mandatory path, no gate, no migration of existing ablation scripts (objectstack-ai#18226's triage 5707288884 set that floor; the migration set measured empty in objectstack-ai#18598's census). ## `.claude/agents/os-dev.md` — one line in, one true duplicate out Inserted, new :254 (119 B), directly under the mutate-then-build line of the ablation step (:253 「每一腿(变异与还原)都是:改动 → 证明它真落到了磁盘 → …」, 108 B) and above the preflight line (old :254, now :255, 113 B): > - 落盘走 `node scripts/ablation-replace.mjs`,不走 `-i` 家族:锚点必须命中,写入与还原按磁盘核验。 「`-i` 家族」 is the name the file's existing :258 already gives the trap (`sed`、`perl -i`、`str.replace`、`re.sub` 零命中也 exit 0). The line is a routing line in the register of the preflight line beside it: one instrument, the reason in the same breath, no 「必须」, no gate. Paid by deleting old :319 (72 B), the opening bullet of 「干净收尾」: > - 报告落两次,GitHub 优先:卡片评论在前,终报消息在后。 What it lost: nothing the file does not state elsewhere, twice. Its three claims — two deliveries, GitHub first, comment before terminal message — are the header's :17 (「报告交付两次,GitHub 优先:先作 issue 评论,…」) and :18 (「…再作为终报消息…」), and the very next bullet of the same section (now :320, 「终报消息之前,把同一段 JSON 发成 issue 评论,…」) restates the ordering together with the marker. The section now opens on that bullet. Why a payment and not a fold: measured on this tree, of the file's 284 adjacent bullet pairs ZERO merge under the 120-byte cap (smallest merged 133 B), and in the ablation block :253–:264 ZERO of 11 (smallest 162 B); the block's total slack is 249 B spread over lines holding 0–61 B each, none of which fits a 35-byte script path plus its reason. Re-wrap funding is refused by the ratchet rule in any case. Whole file: 403 → 403 lines; widest line 120 B before and after; control-byte scan (`grep -naP` over the C0/DEL range) on both files: clean. ## `packages/qa/dogfood/README.md` — one sentence Step 4 of 「Adding a golden test」, placed before the existing preflight sentence (mutate, then prove `dist/`); the helper's usage placeholders are written as words here because the platform sanitizer eats tag-shaped fragments in a PR body — in the file they are the helper's own spellings: > Make the revert itself through `node scripts/ablation-replace.mjs --file PATH --anchor TEXT --replacement TEXT -- CMD` rather than `sed -i` / `perl -i`: the anchor must hit, the write and the restore are verified against the disk, and a mutation that did not land exits non-zero instead of handing you a green run. Non-governed, no line budget, the README's English register. Net +4 lines in that paragraph. ## Verification by the card's own instrument (tree `objectstack-ai/objectstack`) | reading | card @ `ab1d35925` | base `f6189a43f9` | head `fa55c9d0ad` | |---|---|---|---| | `git grep -lI 'ablation-replace'` | 1 as reported (that sha itself answers 0 — the helper landed in `d0b8ec2aaf` after it) | 1 (the helper itself) | **3** — `.claude/agents/os-dev.md`, `packages/qa/dogfood/README.md`, `scripts/ablation-replace.mjs` | | `git grep -lI 'ablation-dist-preflight'` | 7 | 8 (the card's 7 plus the helper's own header, which names its sibling) | 8, unchanged | | control `git grep -cI 'dogfood'` os-dev.md / README.md | 1 / 6 | 1 / 6 | 1 / 6, unchanged | | `git grep -cI 'ablation'` on `.claude/skills/dogfood-verification/SKILL.md` | 0 | 0 | 0 — the third routing document named in objectstack-ai#18598's report does not exist; not touched | Per-file hits at head: `ablation-replace` — os-dev.md 1, README.md 1, the helper 14; `ablation-dist-preflight` — os-dev.md 1, README.md 1. ## objectstack-ai#18226 option A — a reading, not implemented here Option A (「在字节纪律里加一条具名陷阱」: name the `perl -0` + `$/`-interpolates-to-NUL trap in the byte-discipline rules) was **routed, not ruled, and has not landed**. objectstack-ai#18226's triage 5707288884 routed it to the skills seat (「选项 A(在字节纪律里加一条具名陷阱)要走 skills 席,⛔ 不在本卡内落地」), the claim 5710594253 repeated the boundary, and PR objectstack-ai#18598 landed option C (the helper) only. On this tree: `AGENTS.md` carries no control-character or `check:nul-bytes` sentence at all (its only `perl -i` mention is :224, the Bash-guard write shapes); `os-dev.md` :387–:390 (字节与 sanitizer 纪律) covers writing control characters as escapes and the harm of a raw NUL, and :258 covers mode ② (nothing written, exit 0) — neither names mode ① (`-0` + `$/`). Left untouched here (this card is the pointer only) and reported in `open_questions` for the seat to queue. ## Gates (worktree at `fa55c9d0ad`; every exit code captured by redirect-then-`$?`, never through a pipe) Derived with `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` from the worktree, no hand-fed paths (change set vs merge base `f6189a43f`: the two files; committed 2, working tree 0, untracked 0). 53 commands; reconciled with `--ran` carrying `:: exit N` per line: **53 derived, 48 run, 5 NOT-MEASURED, 0 UNRUN**. Named by the dispatch: - `pnpm check:pm-skill-ratchet` :: exit 0 — 「.claude/agents/os-dev.md is 403 lines (ceiling 403; headroom 0)」 · 「widest table row is 0 bytes (pin 0; headroom 0)」 - `pnpm check:nul-bytes` :: exit 0 — 「OK (scanned 8814 text file(s) … no raw ASCII control bytes)」 - `pnpm check:agent-model-declared` :: exit 0 — 「1 agent definition(s) under .claude/agents/ all declare a model」 - `pnpm check:pm-governed-merges` :: exit 0 (the post-merge audit). `node scripts/pm/check-governed-merges.mjs --pr NUMBER` needs this PR's number, so it runs after creation; its verdict (expected GOVERNED, exit 3, which is a verdict and not a failure) is recorded in the report comment on the card. - `pnpm lint` (repo-wide, `eslint . --no-inline-config`, as PR objectstack-ai#18654 ran it) :: exit 0, 100 s, on `fa55c9d0ad` The other 43 that ran, all exit 0: `check-ci-filter-parity`, `check-closing-keyword-parity` (+ `--self-test`), `check-comment-mask-adoption` (+ `--self-test`), `check-comment-mask-corpus`, `check-keyed-text-bounds` (+ `--self-test`), `check-platform-object-tenancy-census` (+ `--self-test`), `check-plugin-teardown-shape` (+ `--self-test`), `check-registry-log-declared` (+ `--self-test`), `check-rest-log-spy-declared` (+ `--self-test`), `check-system-context-census` (+ `--self-test`), `check-undeclared-dep-imports` (+ `--self-test`), `docs-audit/check-affected-docs`, `docs-audit/check-drift-comment`, `pm/check-governed-queue-guard --self-test`, `pm/check-harness-current --self-test`, spec `check:empty-state` · `check:liveness` · `check:strictness-ledger` · `check:variant-docs`, `check:agent-test-spelling`, `check:commit-card-trailers`, `check:cross-package-test-inputs`, `check:doc-authoring`, `check:driver-memory-census`, `check:org-identifier`, `check:page-declaration-shape`, `check:pm-skill-id-lint`, `check:published-files`, `check:refd-timer-probe`, `check:skill-frame-sync`, `check:slot-lookup`, `check:test-source-alias`, `check:tier-file-adoption`, `check:type-source-resolution`, `check:watch-hint-literal`. NOT MEASURED (exit 3, `PREREQUISITE NOT MET` — each loads BUILT artifacts and refuses on this unbuilt worktree; the diff touches no source, config or manifest, so their verdict is CI's on its correctly built tree): `pnpm --filter @objectstack/lint run check:doc-formula-expressions` (`@objectstack/formula` and `@objectstack/lint` not built), `pnpm check:dts-closure`, `pnpm check:dual-build-cjs-loads` (101 packages without `dist/`), `pnpm check:lean-entry-closure` (`packages/objectql/dist` absent), `pnpm check:sourcemap-no-sources-content`. Their only remedy is a whole-repo `pnpm build`, not proportionate to a prose-only change; declared, not hidden. ① / ②: the only package touched is `@objectstack/dogfood` (private, no `files[]`, scripts `typecheck` + `test` only); the README is an input to neither (the sole `README` mention under `packages/qa/dogfood/test` is a comment in `rls-fixture.dogfood.test.ts:29`), so no closure build and no package test is owed — a declared narrowing. `skip-changeset`: `.claude/**` is on the fast lane and `@objectstack/dogfood` is private with no `files[]` — nothing published moves. ## Acceptance notes - noted, not filed: the card attributes 「`git grep -lI 'ablation-replace'` → 1」 to `ab1d35925`, but that sha itself answers 0 — the helper landed in `d0b8ec2aaf` (objectstack-ai#18598) after it; the reading was evidently taken on a tree that already carried the merge. The premise is unaffected: base `f6189a43f9` answers 1 (the helper alone). 承接者:无 — recorded here and in the report. - noted, not filed: `ablation-dist-preflight` answers 8 on the tip, not the card's 7 — the eighth is the helper's own header cross-referencing its sibling, which is the intended composition (the two tools prove different halves and say so). 承接者:无. - Rider not taken: the pending `tail --pid` rider in 干净收尾 (named by PR objectstack-ai#18659) stays for the next os-dev.md PR; this PR's deletion in that block is the duplicate opener, not a slot for it. ## 维护者速读(草稿) **改了什么** — 只动两处文档。`.claude/agents/os-dev.md`(行数不变 403 / 403)在消融步骤里加一行,把 dev 指向 PR objectstack-ai#18598 新落地的锚点核验替换助手 `scripts/ablation-replace.mjs`,与已有的 `ablation-dist-preflight.mjs` 指向并排、按消融实际执行顺序(先落盘变异、再证明到达 `dist/`、再读结果);付账是删掉「干净收尾」节开头一条与文件头部及同节下一条重复的「报告落两次」句。`packages/qa/dogfood/README.md` 的「Adding a golden test」第 4 步加一句同样的指向。 **为什么改** — 这个助手落地了,却没有任何一份指令文档指向它;而指向其姊妹工具的文档有两份。一个采纳式工具若无人被指向,只有碰巧 grep 过 `scripts/` 的人会用到 —— 这正是卡 objectstack-ai#18226 要消灭的「靠记性」。⛔ 不做成必经路径、不加门禁、不批量迁移旧消融脚本(objectstack-ai#18226 分诊已定这条地板)。 **风险与代价(含回滚)** — 零代码、零 changeset、零门禁改动;唯一的内容损失是被删的那条重复句,它的三个主张在文件头部(:17–:18)与同节下一条(:320)各有原文。回滚 = revert 本 PR,无其它文件牵连。 **席位意见** — (留空,席位定稿) **你要做的** — 读新 :254 一行与被删 :319 一行;批准即由席位落地。若不同意付账形态,点名另一条重复句,席位代改。 --- _Generated by [Claude Code](https://claude.ai/code/session_01Gqi43smmqjJ5sUrhfoPeKu)_ Co-authored-by: Claude <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #18379
H19 gathered
Blocked-by:targets over the body ∪ comment union and judged the expiry against the body's set (#17564), but theUNJUDGEDbranch never moved with that split: it stayed founded on the resolution set, so a target a seat had superseded in the body could still veto the verdict from the comment archive whenever its number was unreadable. Two conditions had to coincide, which is why it outlived #17564 — the target is superseded (judging correctly ignores it) and the superseded number 404s (resolution cannot dismiss it either) — and the resulting row was unclearable by construction: the card's owner had already done the prescribed thing, and the only remaining lever, editing the archived comments, is the write the protocol forbids because those comments record what an earlier seat measured and why the line was migrated.Authored by
session_01Gqi43smmqjJ5sUrhfoPeKu(Claude Code). Every reading below was taken on this branch atab30706ed, the head this body describes.The two sets, before and after
blockerTargetsForis untouched: DISCOVERY stays a union, because H26 and H28 share those resolutions and a comment-borne target is a perfectly good answer to what they ask. What changed is insideh19BlockOutlivedBlocker.UNJUDGEDUNJUDGED(unchanged)The denominator moved with it: an archive-only unreadable target no longer enters the row's
N of Mcount, which was the other half of the sentence the card quotes (「1 of 2 … the set judged against the BODY's set」 on a card whose body names one target).What the archive is still consulted for, and must be: evidence that the wait may still be RUNNING. An OPEN target it names still withholds a discharge, and an UNREADABLE one now withholds a FULL discharge in a new branch (「No target this row JUDGED is still open, but N target(s) the comment archive names … could not be read at all」). Narrowing that half to the body too would re-create #11747 — the measured mirror defect, where a re-park parks the live blocker in a comment while the body still names the spent one, and a card was released to
pm:queuewhile its real blocker was open and dispatched. Only the power to FOUND a verdict is carrier-scoped; evidence of a live wait never is.⛔
UNJUDGEDis not silenced. Three controls say so live, below.Pins
Battery
H19 judged-set founding— the file's second registered battery (SELF_TEST_BATTERIES), 37 cases registered, pinned at 34;SELF_TEST_BATTERY_FLOORraised1→2, so deleting either battery now fails the roster rather than shrinking a total nobody reads.It floors a NEGATIVE — a row that must not fire — plus the controls that keep the silence scoped:
Blocked-by: #18336, the archive naming#13515, resolved 404) ⇒ discovery still yields both targets, and no row of any kind;UNJUDGEDfires, counted1 of 1, still refusing to be read as a quiet row;UNJUDGEDfires;1 of 1, full discharge;pm:blockedcards: 32 parkBlocked-by:outside the body (invisible to the unlock scan), 16 wait on already-closed upstreams, 7 name no blocker at all #11747's mirror ⇒ an OPEN archive target still withholds, still1 of 2, stillPARTIAL;Two existing pins were re-judged rather than deleted, and both are called out where they sit:
H19 precedence: an UNRESOLVED comment-borne target is NOT silenced by the bodyasserted exactly the row this card measures as unclearable. Same fixture, re-judged to the direction the file's own asymmetry table always declared: it now founds no row beside a body set.H19 superseded: the clause rides the UNJUDGED branch as wellused a fixture whose body named a target that appeared in no resolution. Its unreadable target is now the BODY's own, which is what the pin was always about: a hygiene clause can only ride a row something in the judged set founded.One prose correction rides along: the summary line's H19 shortfall clause promised that every unresolved target is named on its own card's row. That clause was measured false once already (#11218) by promising more than the rows deliver, and a repair that narrows what a row founds owes it a second correction — it now names the archive-only class it no longer founds a row for.
Ablation — from the committed fix, restored by blob hash under a trap
Mutation: the founding filter
state === 'unresolved' && carries(r)reverted tostate === 'unresolved', on disk, atHEAD=ab30706ed.Direction: RED, as predicted, and the reds are the right ones — five negatives plus three counts:
The
gotvalue on the card's own fixture is the measured sentence, reproduced verbatim by the ablated code:Self-test
Run under
scripts/pm/os-verify-lock.sh(VERDICT command-exit 0), as were the ablated run and the live sweep.Live sweep — report-only, from this worktree, after the fix
node scripts/pm/check-half-states.mjsat 2026-09-17T12:25Z, headab30706ed, under the verify lock (VERDICT command-exit 0, held 366s): swept 409 open pm-/p0-labeled issues, 462 half-states, 12 H19 rows.Before — anchor #9857 as the scheduled patrol rewrote it at
2026-09-17T07:50:06Z(four H19 rows, allUNJUDGED, which sort above the body's size trim):11973,11975,15140,15214.After — this run:
#18336) — the two that were always true of it.UNJUDGEDon#13689(HTTP 404) — the same unreadable epic, reached through that card's body. This is the scope note on the card, and it is the control that proves nothing was silenced.UNJUDGEDonobjectstack-ai/cloudtargets (HTTP 403, repo not readable to this credential) — the standing accepted cross-repo class, untouched.numberin the analytics response, and a metric tile ignores theformatstyle it is handed — measured on 17.3.0 #15768, lint: refuse an incoherent dataset measure (aggregate × field type) at authoring time, from the spec's compatibility matrix (lint leg of #16099) #16354):is CLOSED … the block has outlived its blocker.Blocked-by:set is outside what H19 judges, so it rides a row rather than founding one」.Gates
Derived from this worktree with
node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack scripts/pm/check-half-states.mjs(list re-derived after the commit: identical), every command run, each exit code captured by redirect-then-$?, reconciled with--ran:All 38 exited 0. In full:
skip-changeset:scripts/pm/**is the fast lane — nothing in any package'sfiles[]moves, and the patrol script is not published.Acceptance notes
h26BlockOnIndefiniteTarget) reads the same union with no carrier split at all, so a target superseded in the body can still found an indefinite-wait row there. It is a different row with a different founding question and no measured instance was taken in this act, so it is an observation, not a card. Carrier: whoever next opens H26.Generated by Claude Code