Repository navigation
fix(pm): every --pair run states the inputs it judged from - #18681
Merged
Merged
Conversation
`check-clause2-carriers --pair` answered 0 / 4 / 0 on one pair with an identical script blob, and nothing either run printed could settle which of them had read what. The judging half is already deterministic given a fixed document (`--pair-json` proves it); what was unpinned is what document the LIVE path builds. Every run past the board resolution now closes with a fenced `clause2 input record` block on stderr — the board and which of the three sources answered, the read path and every request it issued with its row count, the pair and the evidence it was derived from, the comments read by count/ids/newest, the claim comment SELECTED as the carrier with the rule that selected it and every candidate it rejected with a reason, each pooled claim's body fingerprint, the PR-body line, and this file's own blob hash and path. The field roster is the same on exit 0, exit 4 and every refusal, so two runs that disagree are diffed rather than re-run. The `--json` sweep carries the same record under `inputs`. No verdict, row, count or exit code reads any of it. Claude-Session: https://claude.ai/code/session_01Gqi43smmqjJ5sUrhfoPeKu Co-authored-by: Claude <noreply@anthropic.com>
This was referenced Sep 17, 2026
[PM seat] domain:skills — 🟢 sunhaolin · session_01QBcLjVCmcWbZEk6qhYMib2 · serial (maintainer)
#7623
Open
os-justin
marked this pull request as ready for review
September 17, 2026 14:41
This was referenced Sep 17, 2026
akarma-synetal
pushed a commit
to akarma-synetal/framework
that referenced
this pull request
Sep 28, 2026
… list with a counterfactual pin (objectstack-ai#18700) Fixes objectstack-ai#18560 **Clause-②: no** `scripts/pm/check-widening-tells.mjs` is the TELL half of the clause-② gate (the DECLARATION half, `check-clause2-carriers.mjs`, is a different file and PR objectstack-ai#18681's subject). Its T1 reader decides a line declares a schema member by what the property's VALUE opens with, and that vocabulary was five alternatives inside one 130-character regex literal. A form missing from it is not a line judged leniently — it is a line that is not a key line at all: `memberTellKind` answers `null`, the row neither fires nor spends the objectstack-ai#16943 replacement budget nor earns it on the removed side, and nothing in the output says so. The silence is indistinguishable from a correct `no`. ## The counterfactual, before and after Re-derived here rather than inherited from the card's reading (objectui#9647 comment 5707064702, an at-tier reviewer's measurement). Run as `PM_SWEEP_REPO=objectstack-ai/objectui node scripts/pm/check-widening-tells.mjs --declaration no --diff …`, the BEFORE leg against the file as it stands at `6dfa3ea77` (this branch's merge base), the AFTER leg at `7f1a81419`: | diff | declaration | BEFORE (6dfa3ea) | AFTER (7f1a814) | |---|---|---|---| | objectui#9647, unmodified | `no` | exit 0, 1 file judged, **0 tells** | exit 0, 1 file judged, 0 tells | | objectui#9647 plus one key added through `stripImportedDefaults(` | `no` | **exit 0, 0 tells** | **exit 4, T1 at `packages/types/src/zod/data-display.zod.ts:584`** | | the same widened diff | `yes` | exit 0 | exit 0 (a `yes` is never blocked) | Row 2 is the red this PR turns. Row 1 does NOT change verdict, and that is the correction the re-derivation forced. ## What the re-derivation corrects The two helpers the card names are objectui's REFUSAL family (`packages/types/src/zod/tombstone.zod.ts`), read off the source: - `retirementTombstone(guidance)` returns `z.never({ error: guidance }).optional().describe(guidance)` — the same primitive as this repo's `retiredKey()`; - `handlerKeyRefusal(key, disposition, label)` returns a `z.custom` predicate typed `never` that returns `false`, and its own docblock records that "The predicate refuses EVERYTHING, a live function included". A key declared through either is a key an author may NO LONGER write. Making those two fire a tell would re-mint, on 290 objectui key lines, the exact false positive objectstack-ai#17955 removed on 255 objectstack ones — and a false tell does not cost a word in a comment, it costs the false `yes` this file's own header refuses to ask an author for. The form that DOES carry a widening, and that no seat had named, is `stripImportedDefaults()` (`packages/types/src/zod/imported-defaults.ts`), whose docblock states its contract as "the same TypeScript type, the same keys, the same checks, the same registry metadata and the same accept set". It returns a LIVE schema and is spelled at 45 key positions on the judged objectui surface. ## The named list `SCHEMA_PROPERTY_FORMS` — exported, frozen, and the constant `SCHEMA_PROPERTY` is BUILT from it. Two fields carry two questions: `pattern` (what makes the line a KEY LINE) and `writable` (whether the key it declares is one an author may write). Counts are key-POSITION counts, each with the tree it was taken against. | form | writable | measured | |---|---|---| | `z.` | yes | 7,784 at objectstack `6dfa3ea77` · 1,482 at objectui `15f01223d` | | `lazySchema(` | yes | 0 at objectstack `6dfa3ea77` (live at DECLARATION positions) | | `strictObject(` | yes | 47 at objectstack `6dfa3ea77` | | `*Schema` | yes | 995 at objectstack `6dfa3ea77` · 56 at objectui `15f01223d` | | `stripImportedDefaults(` | yes | **45 at objectui `15f01223d`** (added) | | `retiredKey(` | no | 255 at objectstack `6dfa3ea77` | | `retirementTombstone(` | no | **187 at objectui `15f01223d`** (added) | | `handlerKeyRefusal(` | no | **90 at objectui `15f01223d`** (added) | | `aliasKeyRefusal(` | no | **13 at objectui `15f01223d`** (added) | The `writable: false` arm is objectstack-ai#17955's decline generalised from one helper name to the family, on the SAME positive, line-local evidence: the value must BE the call and nothing after it. `declaresRetiredKeyTombstone` is renamed `declaresUnwritableKey` accordingly; it is not imported by any other file. **The `no` criterion is not loosened anywhere, and the direction is provable rather than argued.** An unrecognised line reports NOTHING, so no row that fires today can stop firing when the list grows. A self-test case keeps the literal this replaced as the reference and asserts it: every legacy verdict is byte-identical, and the only cells that move are the four added forms moving from "not a key line" to "a key line" — one direction, zero losses. ## The pins New battery, registered on the roster and pinned: `'objectstack-ai#18560 — the declaring vocabulary is a NAMED list, every form pinned by a counterfactual fixture': 30` (39 cases run against a floor of 30). Its unit is the FORM, not the assertion: - a frozen fixture roster is asserted EQUAL to the form set, in both directions and naming the offenders — a form added to the list without a fixture reds, a form silently dropped from the list reds. The fixtures are deliberately NOT generated from the list, which would make every future form pass by construction; - every form is asserted RECOGNISED (`memberTellKind` answers T1) — writable or not, because an unrecognised line is invisible to both sides of the budget, which is the defect itself; - every form's fixture is then driven through `tellsInFile` and asserted against its OWN register: a `writable` form must FIRE with its file:line, an unwritable one must be recognised and DECLINE; - every unwritable form carries the chained-arm control that FIRES, so the decline is bound to the evidence the line carries and never to the helper's name; - the objectui#9647 shape is carried as a case in all three readings, plus the `stripImportedDefaults(` widening with its refusal, its file:line, the `yes` control and the default-board control. `check:entry-guard` is the reason the vocabulary's structural guard is a const initializer's `map` step rather than a top-level `for`: this file exports bindings the sibling gate imports, so a load-time throw would run inside the importer. ## The census — report-only, no re-grade, no state write Has the silence already been relied on? **Zero confirmed `Clause-②: no` landings through these forms, over 11 of 18 rows read.** - **Horizon.** objectui's full history (`git rev-parse --is-shallow-repository` = false, 10,282 commits, initial commit 2026-01-13) up to the checkout tip `15f01223d` (2026-09-16). PR objectui#9647 itself is OUTSIDE it: `git merge-base --is-ancestor 604476d HEAD` exits 1, with the initial commit as the control leg at exit 0 on the same non-shallow checkout. The judgeable part of that window starts 2026-09-10, when objectstack-ai#17278 first let this CLI be told which board it judges. - **Population.** 46 commits add a key through one of the four added forms on `packages/types/src/zod/**`; 18 land inside the judgeable window. - **Coverage — read (11 of 18):** 10 carry `Clause-②: yes` in the PR body (objectui#8884, objectstack-ai#8895, objectstack-ai#8930, objectstack-ai#8967, objectstack-ai#9051, objectstack-ai#9338, objectstack-ai#9495, objectstack-ai#9539, objectstack-ai#9541, objectstack-ai#9565); 1 (objectui#9443) carries a "Clause-② carriers" section attaching `needs:contract-review` with no `yes`/`no` token. - **Coverage — NOT ATTEMPTED (7 of 18):** objectui#8984, objectstack-ai#9254, objectstack-ai#9261, objectstack-ai#9343, objectstack-ai#9544, objectstack-ai#9589, objectstack-ai#9621 carry no declaration in the PR body; the remaining carrier is each card's claim comment, which was not read. **This is NOT a zero** — it is seven rows unread and named. - The commit-local changeset is a weak carrier in objectui: only 2 commits in the whole history carry a `Clause-②` line in a changeset, which is why the census reads PR bodies rather than the tree. ## The ablation From the committed fix, `HEAD` blob `1b741ee6c22db6523475b698d392a8ecd41686d6`, under a `trap '…' EXIT INT TERM` restoring `git checkout HEAD -- scripts/pm/check-widening-tells.mjs` at an absolute path. No build or `dist/` is involved — the gate runs from source, so there is no rebuild leg to prove. - **mutation:** the `retirementTombstone(` and `handlerKeyRefusal(` rows deleted from `SCHEMA_PROPERTY_FORMS`; - **mutation proved on disk, not from an exit code:** anchor counts 1 → 0 for each form, file 257,365 → 256,697 bytes, blob `9fb65edf6f68b1501a0aec29d0ddb4c95bcf44a2` which is not the HEAD blob; - **predicted direction:** RED. **Observed:** RED — `--self-test` exits 1, "6 of 377 case(s) failed" plus the verdict-handshake report. The failures name the mechanism: the orphaned-fixture case prints both dropped forms, both RECOGNITION cases fail, both objectui#9647 cases fail, and the one-direction case fails; - **restore verified by hash, not by a return code:** `git hash-object` back to `1b741ee6c22db6523475b698d392a8ecd41686d6`, `git diff HEAD` empty, `git status --porcelain` clean, and the suite back to 381 cases pass. ## Self-test `pnpm check:pm-widening-tells` — **381 cases pass** (342 before this PR; +39). Exit 0. The two gates that IMPORT this module were run too: `check:pm-clause2-carriers` exit 0, `check:pm-prior-rulings` exit 0. ## Derived gates `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` from the worktree, no hand-fed path list — 29 commands, each run with `$?` captured BEFORE any pipe. Reconciled with `--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 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-widening-tells 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 ``` Repo-wide `pnpm lint` (`eslint . --no-inline-config`) at `7f1a81419`: **exit 0**, no findings. Control-byte scan over the edited file: no hits. `skip-changeset`: `scripts/pm/**` is not published by any package's `files[]`, and this diff touches nothing else. ## One boundary this does NOT close, recorded rather than left to be found A FILE-LOCAL declaring factory. Both trees mint them — `placeholderFree(` (23 key lines), `strictIdent(` (12), `emptyProps(` (9) at objectstack `6dfa3ea77`; `chatbotRequestBodyArm(` (2), `retiredDeclarativeKanbanKey(` (1) at objectui `15f01223d` — and a list of shared, exported helpers cannot name a factory private to one file. A name-shaped heuristic is refused in the header, with the overturn condition written down. Filed as its own finding in the report on the card, not fixed here. --- _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
…ast 100 is UNJUDGED rather than a truncated claim pool (objectstack-ai#18799) Fixes objectstack-ai#18683 Clause-②: no ## The defect `scripts/pm/check-clause2-carriers.mjs` read a card's comment thread with ONE request — `/issues/{n}/comments?per_page=100`, no `page=` ladder, no short-read check — while the two sibling list reads in the same file paged to a declared cap and answered `null` (UNJUDGED, never clean) when they hit it. One file, two OPPOSITE defaults on "I did not read everything", and the fail-OPEN one was the read that arbitrates OWNERSHIP: the governing-claim pool, its membership, and the `Clause-②` declaration read out of it all come from those rows. A thread past 100 comments handed the pool its first page and nothing said the tail had been dropped, so a claim written past row 100 was not superseded — it was never a candidate — and a superseded carrier governed in its place. ## The before-reading, on a 101-row fixture Driven end to end through the real CLI against a stubbed board (`--pair`, no network), on `main` `d9ba33df4c` (script blob `d753e2a8cf06d8f72c436e0a1917b2fecb8be813`). Two fixtures, both 101 rows, both differing from a complete thread only past the page boundary. | fixture | BEFORE (`main`) | AFTER (this PR) | |---|---|---| | 100 claim-free rows, the 101st the only `Claim:` | `card-comments: 100 row(s)` · `claim.selected: none — no comment on this thread carries a line beginning \`Claim:\`` · **exit 4, row C2 `absent`** | `card-comments: 101 row(s)` · the 101st claim is the pool · `claim.clause2-line: DECLARED \`no\`` · **exit 0** | | row 1 an older `Claim:` declaring `yes`, the 101st a newer one declaring `no` | `claim.clause2-line: DECLARED \`yes\`` from the SUPERSEDED carrier, which is not even listed as rejected · **exit 4, row C3** | the newer claim governs, the older is listed REJECTED/SUPERSEDED · `DECLARED \`no\`` · **exit 0** | The second row is the fail-OPEN direction stated as a measurement: one thread, two readings, and they disagree on the declaration itself. ## The ladder, and the cap All three list reads now go through one `pagedListRead` helper — it pages to a declared cap, stops on the FIRST short page (no wasted request), and on the cap files the one shared `pageCapNote` sentence and answers `null`. `readCarrierEvents` (`EVENT_PAGE_CAP` 10) and `readPullFiles` (`FILE_PAGE_CAP` 3) keep their caps to the number; what they gain is that the third read can no longer hold a different default. `COMMENT_PAGE_CAP` is **10** pages = 1,000 comments. Sized on this board, read 2026-09-17 off the open-issue list rows (550 rows listed, cross-checked against `open_issues_count` = 550): - longest open thread of any kind: seat post objectstack-ai#6015 at **895** comments — nine pages; - next four: objectstack-ai#12708 at 365, objectstack-ai#6023 at 241, objectstack-ai#6017 at 206, objectstack-ai#6024 at 187; seat post objectstack-ai#7623 at 71; - longest thread carrying a queue label: objectstack-ai#13799 at **117** (`pm:queue`, p2, unassigned); - longest card in the clause-② population (28 pairs the sweep derived that day): objectstack-ai#17534 at **14**. So ten pages clears the whole board today with a page to spare, and it is the same ten `EVENT_PAGE_CAP` uses — a reader comparing two caps in one file should have to remember one number. ## The input record The diagnosis key stays `comments`, so every sentence already keyed to it still finds its diagnosis. Two declared fields are added to `INPUT_RECORD_PAIR_FIELDS`, one per thread this file reads: ``` pair.1.card-comments: 101 row(s) pair.1.card-comment-pages: 2 of 10 page(s) requested — the ladder stopped on a SHORT page, so the thread is COMPLETE pair.1.pr-comment-pages: 1 of 10 page(s) requested — the ladder stopped on a SHORT page, so the thread is COMPLETE ``` and, when the cap is what stopped the read: ``` pair.1.card-comment-pages: CAPPED — 10 of 10 page(s) of 100 comments each were requested and EVERY ONE came back full, so the tail is past the cap and the thread is UNREAD (UNJUDGED) — ⛔ never a truncated pool, ⛔ never a clean reading ``` A thread of exactly 100 rows and a thread whose tail was dropped are the same `100 row(s)` in every other line the block prints; they differ here, because the complete one stopped on a short page and the truncated one did not stop at all. The request ledger PR objectstack-ai#18681 added shows the same ladder from the other side — request objectstack-ai#3 is now `…/comments?per_page=100&page=1` and objectstack-ai#4 is `&page=2`. ## The pins A new `--self-test` battery, `objectstack-ai#18683: the card-comment read pages to a cap — past 100 is UNJUDGED, ⛔ never a truncated pool`, 27 cases, declared in `SELF_TEST_BATTERIES` with the roster floor raised 29 → 30. It drives the ladder with an offline page server that reproduces GitHub's own semantics and counts the requests; ⛔ no network. What it holds: the 101st claim ENTERS the pool and GOVERNS, and its line is what the limb reads; the same thread cut at 100 reads `absent` (the CONTROL — the reading the un-paged read produced); the newer claim past the boundary supersedes the older one inside it, and cut at 100 the superseded carrier's `yes` is what the limb reads; a capped read is `null`, which is neither `missing` nor `absent` nor a carrier but `unreadable`; the ladder stops on the first short page (2 requests for 101 rows, 1 for a short thread, 2 for exactly 100 — a full page is indistinguishable from a finished one); a page that came back unread ends the ladder and the record says the cap was NOT what stopped it; the input record declares and prints both ladder fields; the sibling caps are untouched; and all three reads render ONE cap sentence. ## The census, and the triage's upgrade probe Report-only, no state write. Over the 550 open rows (521 issues, 29 PRs) read on 2026-09-17: - open `pm:queue` / `pm:dispatched` cards: **274**, of which **1** exceeds 100 comments — objectstack-ai#13799 at 117; - all open issues over 100 comments: **8** — objectstack-ai#6015 (895), objectstack-ai#12708 (365), objectstack-ai#6023 (241), objectstack-ai#6017 (206), objectstack-ai#6024 (187), objectstack-ai#6021 (145), objectstack-ai#6367 (127), objectstack-ai#13799 (117). Seven are `pm:seat` posts; - open PRs over 100 comments: **0**; the longest is objectstack-ai#18638 at 10. **The upgrade probe's result: the condition is NOT met today.** The clause-② population is what a `--pair`/sweep derivation actually pairs, not what carries a queue label: the sweep derived **28 pairs from 29 open PRs**, and the longest card thread among them is **14** rows (objectstack-ai#17534). The two open PRs that mention a 100+-comment card in prose — objectstack-ai#18786 (objectstack-ai#6015, objectstack-ai#7623) and objectstack-ai#18765 (objectstack-ai#6024) — deliver objectstack-ai#18693 and objectstack-ai#18652 respectively, both under 10 comments; driven live before and after, both answer exit 0 with an identical pair reading. So no recorded `--pair` verdict on this board today was taken on a truncated pool, and the triage's p1 condition (「找到任一进入条款②认领池、评论数 > 100 的卡并驱动一次」) has no live instance to drive. The exposure is one PR away rather than realised: objectstack-ai#13799 is `pm:queue` at 117 and enters the population the moment a PR delivers it. The cost is unchanged by the ladder, measured on the same board: **64 reads for 28 pairs, before and after**, because every live thread fits one page and the ladder stops on a short page. The live `--pair 18765` input records differ in exactly three lines — the two request paths gaining `&page=1`, and the two new ladder fields.⚠️ One thing the two full sweeps do NOT compare: the sweep's finding COUNT moved 4 → 3 between them, and that is the board, not this diff. `needs:contract-review` was hung on PR objectstack-ai#18792 at `2026-09-17T21:04:46Z`, between the two runs, closing the C1 split on objectstack-ai#18792 / objectstack-ai#17541 on its own. The controlled A/B is the `--pair 18765` diff above. ## The ablation Two legs, each from the COMMITTED fix, each proving the mutation reached disk by blob hash and occurrence count before reading any result, each restored under a `trap` with `git checkout HEAD --` and verified by hash and an empty `git diff HEAD`. HEAD blob `ccd5ad7c9a00fe703d644be261a24f1ed847915a`. | leg | mutation | blob after | self-test | |---|---|---|---| | A — the ENTRY side | `COMMENT_PAGE_CAP` 10 → 1 | `1a0a952d07748101937420006b9475042d93a5cb` | **exit 1, 11 of 865 red** — the 101st-claim pins, the superseding pins, the request-count pins, the input-record pin | | B — the UNJUDGED side | the cap branch returns the pages that DID arrive (the pre-fix fail-OPEN default) | `c176dfe7e54e7de6bc45737487841a346f509b79` | **exit 1, 3 of 865 red** — a capped read is no longer `null`, no longer `unreadable`, and files no cap sentence | Every red in both legs belongs to the new battery; nothing pre-existing went red in either. A third, unplanned reading came for free: leg B's first attempt was a `perl -0pi` substitution whose anchor contained a `/`, so the edit silently did nothing — the on-disk proof refused it with `ABLATION VOID: the edit did not reach disk` instead of reporting a green as a measurement. ## Self-test ``` ✓ check-clause2-carriers self-test: 865 cases pass ``` 838 before, 865 after — the 27 the new battery registers, which is what its floor pins. ## Derived gates `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack`, no hand-fed path list, re-derived after each `origin/main` merge (identical list both times). All 34 run at head `993cb89e18`, each exit code captured by redirect-then-`$?`: ``` 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 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: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 pnpm lint :: exit 0 ``` `--ran` reconciles 34 derived / 34 run / 0 UNRUN. `pnpm check:pm-dispatch-gates` was run detached to a file — 1,788 cases, 748.6s on this box — and waited on in the foreground rather than under a timeout, so it is a measurement and not a SIGTERM. ## Out of scope, deliberately objectstack-ai#18764 (a decorated `**Claim:**` never enters the pool — the ENTRY side) was read and NOT folded in: this card is WHICH rows reach the reader, not what the reader does with them, and the two repairs touch different lines. `claimRetractions` (PR objectstack-ai#18770, the EXIT side) was read for the words it uses and not touched. The header's request-budget paragraph is amended in the same commit, because it stated "2 reads per card" as a fact and the thread is now a ladder — a cost statement that stopped being true is the shape this file exists against. `skip-changeset`: `scripts/pm/**` ships in no package's `files[]`, so nothing published moves. --- _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 #18456
Clause-②: no
scripts/pm/sits outside every workspace package, and the root package is private, so nofiles[]can ship this diff —skip-changeset.The defect
check-clause2-carriers --pairis the landing pre-check every seat runs, and on one pair(PR #17917 / card #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-jsonproves that); what was unpinned is whatdocument the live path builds. This states it: every
--pairrun now closes with a fencedclause2 input recordblock on stderr, with the same field roster on every exit, so two runsthat 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_FIELDSandINPUT_RECORD_PAIR_FIELDSand 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 onindented lines under their key.
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.derivedpair.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-lineFour of them are worth naming for WHY they are there:
run.requests— every read the run issued, in order, with its channel, its exact pathand its row count. A page asked for with
per_page=100that answers with exactly 100rows 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 itand 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/0actually needs. A
misplacedverdict on that thread requires the governing claim to havecarried 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 blobwas 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 reads25d204236aa8296644813109fa77541d6efe1644,which is exactly
git rev-parse HEAD:scripts/pm/check-clause2-carriers.mjs.The
--jsonsweep carries the same record underinputs— the same record, ⛔ never a secondformat.
The two live blocks the card names
--pair 17917— the pair from the card. Both it and #18654 have since merged, so--pairanswers 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:
--pair 18654— the pair this seat landed today, which answered 0 at 12:32Z and is likewisemerged now (exit 2):
⭐
diffof those two blocks is four lines:run.utcandrun.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:Pins
Battery #18456: the
--pairinput record — the same block on every exit, so two runs thatdisagree can be diffed, registered in
SELF_TEST_BATTERIESwith a floor of 38; 41cases register.
SELF_TEST_BATTERY_FLOORraised 26 → 27 by exactly the one battery this adds.What is pinned, in the card's own terms:
a refusal that formed no pair — all three key lists asserted equal;
(with
INPUT_RECORD_UNSET), an empty record still carries every declared key, and a keyoutside the roster is named rather than printed in silence;
claimCarrierSelectionapplies — so the printed rule cannot drift from the applied one;
--pair-jsonrun names that read path as such and names the document;the measured shape, asserted directly;
gitBlobSha1is pinned against two valuesgit hash-objectprints.⛔ 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
cardDeclarationjudged (ONE derivation —
cardDeclarationnow callsclaimCarrierSelectioninstead of derivingthe pool inline, so the record and the verdict cannot describe two different comments).
--self-teston this head: 786 cases pass, exit 0 (745 before; +41).Ablation
From the committed tree, blob
25d204236aa8296644813109fa77541d6efe1644(= this PR's headblob), the pair half of the record removed on disk, mutation proved before the run, restore by
blob hash under a
trap:Direction predicted before the run and observed: turns red. The module is run directly from
source by
node scripts/pm/…— no build and nodist/between the edit and the run, so theon-disk proof is the whole preflight.
cannot see
main's emission. An ablation that deleted the two lines inmain'sfinallywould 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.
a5ed18ced(2026-09-12T06:05:25Z, "a key-INITIALclause-② line that QUOTES the spelling is not a declaration"); its next change was
4e3a496baat 2026-09-13T17:19:18Z, after all three runs.a5ed18ced's blob isaecbb2d86683eb908468fdacaac2ff53753f06ef— the same blob PR fix(pm): the clause-② correction remedy names THIS card's claim comment, never a specimen from another card #18448's body independently citesas "the exact blob the 2026-09-13 readings were taken from".
ObjectPermissionSchema's retiredallowRestore/allowPurge: only literalfalseparses (not a truthy/falsy split), and no post-parse guard can ever see either key #17425 at that moment was comment5650083758(2026-09-13T01:57:23Z). Its line 3 opens
Clause-②: no —and then quotes the spelling againinside the same line. Run first-hand against that historical blob's own
readClause2Line:{"kind":"near-miss","reason":"describing"}, andcardDeclarationon aone-claim thread reads
missing— ⛔ not a declaration. Today's copy reads it identically.--pair-jsondocument assembled from the REAL thread as it stood at 03:04:09Z (its 9comments, both carriers' real label event streams) answers exit 4, MISPLACED on this PR's
head, quoting the superseded
Clause-②: yesand naming5650083758as the correction target —which is what the 03:04Z reviewer and the 02:53Z dev round both reported.
the record's
claim.selectedfingerprint andclaim.clause2-linewould have shown, had the0-runs printed one.
readClause2Line(selfcontained) and
cardDeclaration(which imports today's sibling modules); the commit orderingis read from a shallow checkout, corroborated by fix(pm): the clause-② correction remedy names THIS card's claim comment, never a specimen from another card #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.
readCardCommentsissues ONE request,/issues/N/comments?per_page=100, withno
page=ladder and no short-read check.readCarrierEventsandreadPullFilesboth page toexhaustion and answer
null(UNJUDGED, never clean) when their cap is hit, for the reason theirown 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 #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
#3prints its row count, so a(100 row(s))on aper_page=100request is nowreadable. ⛔ Not fixed here; the seat files or re-scopes.
Gates
Derived with
node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackfrom 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-$?:Reconciled:
dispatch-gates --ran⇒ 34 derived, 34 run, 0 NOT-MEASURED, 0 UNRUN.Repo-wide
pnpm lint(eslint . --no-inline-config) atf5773ce08: exit 0.grep -naPfor 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:
--pair-jsonusagerefusals (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 thedirection the file's own header argues for. Carrier: whoever next edits
main.main's--pairvalue is parsed in two places now (once forrun.mode, once for the pairitself). Both read the same argv through
flagIndex; a reader may prefer one. Carrier: whoevernext edits
main.🤖 Generated with Claude Code
https://claude.ai/code/session_01Gqi43smmqjJ5sUrhfoPeKu
Generated by Claude Code