Repository navigation
ci: one Test aggregator becomes the required test context, shards 4 -> 8, dist pins get their own job - #9584
Conversation
… -> 8, dist pins get their own job `ci.yml`'s `test` job had grown into its own `timeout-minutes: 20`. Over the 14 most recent `merge_group` runs and the 14 most recent `pull_request` runs the longest *succeeding* shard-1 job measured 1199 s against the 1200 s ceiling — a one-second margin — and two runs crossed it and were CANCELLED, one of them dequeuing a pull request whose tests had passed, because the merge queue cannot tell `cancelled` from `failure`. Widening the matrix was blocked on the required-check set naming the legs one by one: 4 -> 8 renames four live required contexts, which no workflow edit can do and which leaves every pull request blocked on a check that can no longer report. So the three changes land together. - `test-aggregate` (`name: Test`) — one aggregator job, `needs: [test, test-dist-pins]`, `if: always()`, the single required test context. - `test` — matrix 4 -> 8, `pnpm test --shard=N/8`. The ceiling is NOT raised, no test is skipped or quarantined, the relevance gate is not widened. - `test-dist-pins` — the shard-1-only built-artifact pin step moves to its own job, carrying the `test` job's relevance gate verbatim (markdown second stage included; asserted byte-equal after comment stripping). `needs.test.result` alone would have been a phantom gate: a matrix job's rollup cannot distinguish "all eight ran green" from "some legs were SKIPPED", and a dependent job without `always()` is itself SKIPPED, which branch protection counts as success. `scripts/check-test-shard-results.mjs` therefore reads this run's own job list from the Actions API and asserts each shard by name and by its own conclusion; `skipped` is not a pass. It fails closed (exit 2, red) and refuses any verdict over a job list not containing its own job. `REQUIRED_CONTEXTS` now names `Test` instead of the four shards; the eight shards and `Test (dist pins)` are classified in `NOT_A_GATE` with the reason. `WATCHED_CONTEXTS` names the set the ruling installs, so the required-check-set patrol reports `drifted` — exit 0, printed, never red — during the window between this merge and the maintainer's third ruleset step; a fixture pins that reading. Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015h79niBMyoB1xcaQje3uiz
…ts the aggregator run `INCIDENT_4959` is `as const`, so `snapshotAt`'s element type is a literal union of the incident's nineteen names. Appending the aggregator run needs the widened shape; `pnpm type-check:scripts` was exit 2 on it. Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015h79niBMyoB1xcaQje3uiz
⛔⛔ MAINTAINER ACTION REQUIRED — this PR must NOT be enqueued in the normal way
Why enqueuing this now would wedge the merge queueThis PR renames the four required test contexts. Per the ruling on objectui#9499 (comment
This tree no longer produces The three steps, in order — steps 1 and 3 are the maintainer's clicks
What earns the confidence — the acceptance was demonstrated, not arguedProto run 35045618251 (instrument branch
Annotations on the failing gate job, verbatim:
⭐ On the very run the gate failed, the matrix rollup reads Serial note
⛔ Generated by Claude Code |
…figs on their own file (objectstack-ai#9636) **Supersedes objectstack-ai#9631** — same commit content, one line of commit message different. That pull request's single commit carried a co-author trailer naming a model; this branch carries the repository's model-free spelling instead. Nothing in the tree moved: both commits hash the same tree, `55f8b4770b2682a6ba0f7ebcd28458f60d45c3e8`. A new branch rather than an amended one because `AGENTS.md` forbids force-pushing outright, and says in as many words that the ban is not graded by whether a branch has a single user. The superseded pull request is left for the dispatching seat to close. --- Fixes objectstack-ai#9519 ## The asymmetry this repairs objectui#9188 rooted the live storage-state **READ** on the spec's own file. The **WRITE**, and the `storageState` option that names the same file to Playwright, still carried a bare relative path — which names no root, so it takes the ambient cwd. | participant | before | after | |:--|:--|:--| | `e2e/live/global-setup.ts` (writes it) | `const STATE_PATH = 'e2e/live/.auth/state.json'` | `join(REPO_ROOT, 'e2e/live/.auth/state.json')`, REPO_ROOT from bare `import.meta.url` | | `playwright.live.config.ts` (names it) | `storageState: 'e2e/live/.auth/state.json'` | `storageState: STATE_PATH` (absolute, rooted on the config file) | | `playwright.import-console.config.ts` (names it) | same bare string — **a third instance**, see below | same repair | | `e2e/live/inline-edit-polish-2572.spec.ts` (reads it) | already repo-root rooted (objectui#9188) | unchanged | ## Measured, not inferred All three readings below come from real runs on this tree, with the **launch directory as the only variable**, a fake sign-in endpoint standing in for the backend, and the preinstalled Chromium. **1. The doubled path reproduces.** Real `playwright.live.config.ts` + real global setup, started from `e2e/`, before the repair: ``` [live-e2e] authenticated as admin@objectos.ai; storageState written to e2e/live/.auth/state.json PROBE cwd=/…/e2e configStorageState=e2e/live/.auth/state.json repoRootedExists=false Error: ENOENT (repo-root read): /…/e2e/live/.auth/state.json on disk: e2e/e2e/live/.auth/state.json ``` Same command from the repository root: green, file at `e2e/live/.auth/state.json` — so the instrument discriminates and the failure is the cwd, not the probe. **2. The config half was unmeasured; now it is measured, and it needed the repair.** Playwright 1.62.1 resolves a relative `use.storageState` against the **process cwd**, never the config directory. Probe with both copies present, run from a subdirectory of the config dir: the **cwd** copy won. Control, with only the config-dir copy on disk: `Error reading storage state from state/probe-state.json: ENOENT`. Two neighbouring options behave the other way and were left alone: `testDir` and `globalSetup` both resolved against the config directory in the same runs. **3. After the repair, from `e2e/`:** ``` [live-e2e] ... storageState written to /…/objectui-issue-9519/e2e/live/.auth/state.json PROBE cwd=/…/e2e configStorageState=/…/e2e/live/.auth/state.json repoRootedExists=true 1 passed ``` No `e2e/e2e/` tree is created. Control from the repository root: also green. **4. Ablation (one-shot, restored).** With the fix committed, `git checkout` of the base revision put the two bare strings back on disk (grep-confirmed before the run); the probe went red with the doubled path again, and the new content pin went `2 failed | 3 passed` — the two reverted files failing, `playwright.import-console.config.ts` still green because it was not reverted. Restored with `git checkout HEAD -- …`; `git diff HEAD` empty and both blob hashes equal to their HEAD blobs. ## The third instance (⭐ reported, and repaired here) `playwright.import-console.config.ts` carried a byte-identical `storageState: 'e2e/live/.auth/state.json'` and shares the same `globalSetup`. It is the same defect class, the same mechanical repair, and no open pull request holds that file (checked over all open PRs, 1765 filenames examined; positive control on the same instrument: `.github/workflows/ci.yml` is held by PR objectstack-ai#9584). Repairing the live config and leaving this one would have left the card reproducible one config over. ## What is NOT done here, deliberately The path-roots gate's population is `TEST_FILE` — `*.test.*` / `*.spec.*` — so a `global-setup.ts` and a `*.config.ts` match neither, which is how its registry could reach zero while these instances sat one directory away. **Widening that population is not in this pull request**: its pin test is held by PR objectstack-ai#9584, and the scope decision belongs on a card of its own. Instead, `scripts/__tests__/live-e2e-storage-state-roots-9519.test.ts` holds the four known ends of this one path together by content, and states in its header that the gate-population question is open. ## Verification - `pnpm exec vitest run scripts/__tests__/check-test-path-roots.test.ts scripts/__tests__/e2e-type-check.test.ts scripts/__tests__/live-e2e-storage-state-roots-9519.test.ts` — 3 files, 42 tests, passed. - `pnpm type-check:e2e` — exit 0 (this project compiles `e2e/**` and the root `playwright*.config.ts` files). - `pnpm lint:root` — exit 0 (32 pre-existing warnings, 0 errors); this task's scope covers `e2e/`, `scripts/` and the root configs. - `pnpm check:test-path-roots` — OK. `pnpm check:control-bytes` — OK. `pnpm check:new-line-citations` — 0 new citations. - `node scripts/check-changeset-presence.mjs` — exit 0, verdict line: "No source or published contract of a released package changed in this range, so no changeset is owed." (the diff touches no `packages/**` file.) - Not measured here: a full live run against a real backend and console — this branch changes only where the state file is rooted, and CI's live lane runs `pnpm test:e2e:live:ci` from the repository root, where the bare and the rooted spelling are the same file. ## Acceptance notes - Noted, not filed: `e2e/live/ci/better-auth-pin.mjs` resolves its app directory from a CLI argument with `path.resolve`. That is an explicit parameter, the documented behaviour of a CLI, and the gate names "a root that arrives as a function parameter" as a deliberate blind spot rather than this defect. Carrier if anyone revisits it: whoever next edits that script. - The seat asked for a third cwd-rooted path if one existed. It did — `playwright.import-console.config.ts`, repaired above rather than left for later. This work was generated by Claude Code in session `session_015h79niBMyoB1xcaQje3uiz`. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_015h79niBMyoB1xcaQje3uiz --- _Generated by [Claude Code](https://claude.ai/code/session_015h79niBMyoB1xcaQje3uiz)_ Co-authored-by: Claude <noreply@anthropic.com>
|
changeset-claim-re-read
|
…bjectui#9562) (objectstack-ai#9690) Part of objectstack-ai#9562 The card reports that `scripts/check-lockfile-dedupe.mjs` returns different verdicts on a byte-identical `pnpm-lock.yaml` while sitting in the blocking `Test (shard 1/4)`. This lands the half that is dev work: **the required lane stops carrying a reading that is not a function of the repository's bytes.** It does not close the card — the residual is named at the bottom and is a maintainer decision. Clause-②: no ## What was actually wrong `scripts/__tests__/check-lockfile-dedupe.test.ts` ran `node scripts/check-lockfile-dedupe.mjs` **twice** — a `pnpm dedupe --check` that resolves against `registry.npmjs.org`. That file is in the `unit` vitest project, which `vitest.config.mts` includes as `scripts/**/*.test.ts` and `ci.yml` shards four ways as `Test (shard N/4)`. All four shards are in `REQUIRED_CONTEXTS` in `scripts/dependabot-merge-gate.mjs`. So a live network resolution decided a **required** verdict on every pull request — including pull requests that touch nothing the reading is about. That is exactly what `lockfile-dedupe.yml` was built to prevent. That workflow is path-filtered to `pnpm-lock.yaml` plus the gate's runtime closure, and its own header says the filter is what keeps the live reading out of the required set under objectstack-ai#3523's rule. The unit test walked around the filter. ## The measurement Lockfile blob held fixed as the control — `sha256` identical before and after every leg, checked each time. Only terms **outside** the repository were varied. pnpm keeps abbreviated and full packuments in two separate caches, so "partial cache" is a real state and both halves were tested. | leg | what varied | exit | verdict | |---|---|---|---| | A | warm shared metadata cache | 0 | `VERDICT deduped` | | B | cold private cache (both halves empty) | 0 | `VERDICT deduped` | | C | abbreviated warm, full-metadata evicted | 0 | `VERDICT deduped` | | D | full-metadata warm, abbreviated evicted | 0 | `VERDICT deduped` | | E | same bytes, registry unreachable | **2** | `VERDICT could not take a reading` | Exit 2 is what the checker documents as "never a pass", and the old assertion was a bare `expect(status).toBe(0)` — so leg E reds a required context, and the failure the reader sees points at `pnpm dedupe` and tells them to commit the lockfile. That advice is wrong there, and the lockfile is shared by every open pull request. **Honest limits on this.** Legs A–D did **not** reproduce the card's red, and I did not identify the card's mechanism — the card's red printed `VERDICT not deduped`, mine prints `VERDICT could not take a reading`. What is reproduced is the **class**: byte-identical repository input, two different verdicts, decided by a term the repository does not contain. Also note the current lockfile blob is `b57d9644` and the card's was `4e954308`, so this is the same property measured on a later tree, not a replay of the card's run. ## The change One file. The live reading stays where the repository already put it — the path-filtered `Lockfile Dedupe Check` context, **still classified BLOCKING** in `OPTIONAL_CONTEXTS`, untouched. `scripts/dependabot-merge-gate.mjs`, `lockfile-dedupe.yml` and the checker itself are all unmodified. The test file now drives the **shipped** script through a stubbed `pnpm` on `PATH`, following the convention `check-doc-snippet-types.test.ts` already uses. The stub records its argv, and **every** run asserts that recording exists — so a spawn that reached a live pnpm fails on the control instead of quietly going to the network. **Coverage goes up, not down.** The two live legs only ever exercised the GREEN path through `main()`. Replacing them covers: - the finding path — exit 1, package names, the `::error title=` annotation, and that a finding never reaches stdout; - the could-not-run path from a real captured registry failure — exit 2, and explicitly **not** 1, so a crash can never be reported as a lockfile finding; - a non-zero exit with no output at all; - the `dedupe --check` argv. This replaces the old `does not rewrite the lockfile it judges` leg with its **cause**: the old leg could only catch a dropped `--check` if the run happened to rewrite something, while this catches it always. ## Ablation — both legs mutated on disk, both restored and proven Run from the committed state; each mutation proved on disk by blob-hash inequality against its `HEAD` blob before the suite ran, each restore proved by blob-hash equality plus an empty `git diff HEAD`. - **Remove the stub from `PATH`** so the real pnpm serves the run: `6 failed | 8 passed`, control message `the stub did not serve this run` fired. Every hermetic test reds on the control rather than silently going live. - **Collapse `classify()` so a crash reads as a finding** (`return 'findings';`): `3 failed | 11 passed` — the checker's own `--self-test`, the registry-unreachable leg and the signal-killed leg. The new assertions discriminate the exact defect class. A first attempt at the mutation was a `perl` no-op; the on-disk hash check caught it and refused to run the suite, which is why it is there. ## Verification - `vitest run --project unit scripts/__tests__/check-lockfile-dedupe.test.ts` — `14 passed`, **1.03s**. The two live legs alone cost 21–31s each warm here, and 71s with pnpm's retry backoff when the registry was unreachable. - `vitest run --project unit scripts/__tests__/` — `170 passed | 2 skipped (172)` files, `4891 passed | 2 skipped` tests. - `tsc -p tsconfig.scripts.json --noEmit` — exit 0. - Gates, each exit 0: `check:control-bytes`, `check:new-line-citations`, `check:test-path-roots`, `check:shell-escape-residue`, `check:entry-guard`, `check:pre-install-import-graph`, `check:comment-mask-corpus`. - `check-governed-queue-guard.mjs --test` on the changed path: `NOT GOVERNED`. - ESLint, narrowed and declared: `eslint --no-inline-config --format json` on the one changed file, 0 errors / 0 warnings, run at `94d803cb8`. Population read from `eslint.config.js` itself, which configures **no** `project` / `projectService` — type-aware linting is not enabled, so a rule's verdict on a file is a function of that file alone and this one-file diff cannot move the verdict on any untouched file. File count `1` read from the `--format json` output, not assumed. The repo-wide `eslint .` run belongs to CI. ## Changeset None. The gate's own verdict line on this branch: ``` Compared the working tree with 61b7553 (merge-base with origin/main): 1 file(s) changed, 0 of them published source of a package the release covers, 0 of them a manifest whose published contract moved, 0 under a package changesets ignores, 0 changeset(s) added. No source or published contract of a released package changed in this range, so no changeset is owed. ``` ## Acceptance notes **What is left of objectstack-ai#9562, and why it is not in this pull request.** The live reading in `Lockfile Dedupe Check` is still registry-dependent: on a pull request that moves `pnpm-lock.yaml`, a registry outage still reds it via exit 2. That is deliberate and documented in the checker — "a reading that could not be taken is NOT a deduped lockfile" — and the only two ways out are `pnpm dedupe --check --offline`, which reds on a cold runner and so trades one spurious red for another, or relaxing the could-not-run verdict, which **is** a relaxation and per the dispatch is a stop-and-report rather than a call made inside this card. Flagged, not taken. **A detection-latency change this makes, stated plainly.** Before, every pull request re-asserted that `main`'s committed lockfile is still deduped. Now that assertion runs on pull requests that touch the lockfile or the gate's runtime closure. This is the path filter's own documented argument — "a pull request that does not touch `pnpm-lock.yaml` cannot change whether that lockfile is deduped, so the filter costs no coverage" — and a pull request that could break the property still meets the blocking gate. The direction it does change: if the property ever drifted **without** the lockfile moving, it would now be caught on the next lockfile-touching pull request instead of the next pull request of any kind. Leg E shows the *verdict* can move without the lockfile moving, so that premise is worth a maintainer's eye; it is noted here rather than resolved. **noted, not filed:** pnpm retries registry failures with backoff (10s, then 1 minute) before giving up — leg E took 71s for that reason. Inside a 20-minute job with an ~800s suite, a sustained registry problem spends that budget before failing. Not filed: it is a property of pnpm's retry policy, not a defect in this repository, and the carrier is the `Lockfile Dedupe` workflow's own timeout derivation, which already flags itself for re-derivation once the workflow has run history. **noted, not filed:** `pnpm-lock.yaml` is currently held by objectstack-ai#8941 and was deliberately not touched. ## Constraints honoured - No deduped `pnpm-lock.yaml` committed; the lockfile is not in this diff. - No test skipped, disabled or quarantined; the file grew from 7 tests to 14. - No CI re-run performed to "confirm a flake". - No new workflow and no new required check; `.github/workflows/ci.yml`, `lint.yml` and `scripts/dependabot-merge-gate.mjs` are untouched and stay with objectstack-ai#9584. - No force-push, no history rewrite. --- _Generated by [Claude Code](https://claude.ai/code/session_015h79niBMyoB1xcaQje3uiz)_ Co-authored-by: Claude <noreply@anthropic.com>
…just the workflows (objectstack-ai#9696) Fixes objectstack-ai#9693 Clause-②: no — widening a lock's population makes it catch MORE, which tightens an acceptance set rather than relaxing one, and publishes nothing. The diff is one test file; no published source, no manifest field, no shipped byte moves (`check-changeset-presence` agrees below). ## What was wrong `scripts/__tests__/check-lockfile-dedupe.test.ts` pinned the live, registry-dependent `pnpm dedupe --check` to one path-filtered workflow — by scanning `workflows` and nothing else. Its own comment states the intent as *"if a required job ever grows a `pnpm dedupe` of its own, objectui#9562 comes straight back"*, but the way a required job **actually** grew one, the time it happened, was through a **test file** — and a test file is not in `workflows`. ⇒ the lock named after objectui#9562 could not have seen objectui#9562 arrive. ## Measured before changing anything — the card's re-check, and where it differs Run verbatim (probe added to `scripts/__tests__/`, spawning the shipped checker unstubbed, no timeout argument), `vitest run --project unit scripts/__tests__/` came back **exit 1 · 170 passed, 1 failed** — and the one red was the probe's own `Error: Test timed out in 15000ms`, ⛔ not the lock. The live reading measures **~25-32s** in this container (`node scripts/check-lockfile-dedupe.mjs` alone: `real 0m32.036s`, `VERDICT deduped`), over vitest's 15s default. So the card's "expect: passes" is not reproducible *verbatim* here — the incidental red is a clock, not a guard. Raising the timeout is exactly what an author meeting that red does. With `120_000` on the probe and nothing else changed, on `29a8a9526`: ``` Test Files 2 passed (2) Tests 15 passed (15) ``` ⇒ a live registry reading, taken from inside the `unit` project — which `ci.yml` shards into REQUIRED contexts — with every assertion in the lock file green. Row 3 of the card reproduces. ## The change One assertion, one widened population — ⛔ not a second lock per class of file, which would only move the blind spot to the next class: - **workflows**, comment LINES stripped, exactly as before; - **every tracked test file**, plus the `vitest.config` / `vitest.setup` modules loaded around them, with comments blanked through `scripts/js-comment-mask.mjs` — the tree's one answer to "comment, or code?". This matters both ways: the file's own header names both hazardous spellings, so without the mask the assertion could never be green. The detector is three markers, and deliberately not the bare word `dedupe` (this repository spends it on UI dedupe keys in hundreds of files): the checker's path; `pnpm` and `dedupe` adjacent in either the shell or the argv spelling; and `check:lockfile-dedupe`, the package script that is a second name for the first. The exemption list is **two** entries and each is checkable: the path-filtered workflow, and this file, whose every run already asserts the `pnpm` stub served it. The file's path is derived from `import.meta.url`, so a rename cannot silently drop either the exemption or the assertion. A companion control drives the detector over each re-introduction route the card enumerates and over prose spelling the same commands, so a marker that stopped matching reds instead of reporting an empty tree as clean. Floors under both reads (workflow count, and the test-file floor `check-test-path-roots.mjs` already publishes — reused rather than re-stated) keep a collapsed population from passing the equality.⚠️ What the scan still does not see is stated in the file header rather than left to read as coverage: a non-test **script** a required job runs which shells out to `pnpm dedupe` itself (the tree's own classification strings quote that command in prose no mask blanks, so that population needs a way to tell a command from a citation first); an invocation assembled at runtime; and an untracked file, since the walk is `git ls-files` — which bites locally, before `git add`, and not in the context the lock protects. ## Ablation — both new routes, rebuilt and proven on disk each leg The repair was committed first; each leg restores under a `trap`, and the restore is verified by `git diff HEAD` being empty, ⛔ not by an exit code. | leg | probe | lock verdict | |:--|:--|:--| | before (on `29a8a9526`) | spawns the checker unstubbed | **green**, exit 0, 15 tests passed | | after, row 3 | same probe, tracked | **red**, exit 1 — the site list gains `scripts/__tests__/probe-9693-live-reading.test.ts` | | after, row 4 | `spawnSync('pnpm', ['dedupe', '--check'])`, `grep -c` for the checker path = **0** | **red**, exit 1 — so the second marker fired, not the first | The failure prints the offending path next to the two that may be there, under the message `a live, registry-dependent reading inside a REQUIRED context — objectui#9562, again`. The probe file was deleted; the working tree is clean. ## Checks run locally | check | verdict line | |:--|:--| | `vitest run --project unit scripts/__tests__/` | `Test Files 170 passed \| 2 skipped (172)` · `Tests 4892 passed` · exit 0 | | `node scripts/check-changeset-presence.mjs` | `✅ No source or published contract of a released package changed in this range, so no changeset is owed.` | | `node scripts/check-test-path-roots.mjs` | `✅ check-test-path-roots: OK (2080 filesystem call(s) in 438 of 3211 test file(s) …)` | | `node scripts/check-control-bytes.mjs` | `✅ check-control-bytes: OK (scanned 7801 tracked text file(s); skipped 85 binary)` | | `node scripts/check-comment-mask-corpus.mjs` | `1 file(s) disagree, within the residue objectui#7882 is holding open` — unchanged by this branch, and the new regex character classes carrying quote characters are the shape that sweep exists to catch | | `eslint scripts/__tests__/check-lockfile-dedupe.test.ts` | exit 0, no output | Heavy runs went through the shared verify lock. `scripts/__tests__/` is the blast radius: nothing outside it reads this file, and the diff touches no runtime source. ## Acceptance notes - ⛔ No new workflow and no new required context: the repair is entirely inside the existing assertion's population, so the hard constraint around PR objectstack-ai#9584 is untouched. That PR does hold `.github/workflows/ci.yml`; this branch holds no workflow file, and the new assertion reads no context NAME, so the two do not overlap. - Noted, not filed — this file's header describes the required test contexts as four `Test (shard N/4)` jobs. PR objectstack-ai#9584 is the change that would move that description, and it already holds the file that defines them; carrier: that PR. ⛔ Not repaired here: it is pre-existing prose outside this card's boundary, and re-wording it from this branch would collide with it. - The card's own re-check recipe reads "expect: passes"; as run verbatim in this container the probe reds on a 15s test timeout before the lock has anything to say. Worth knowing for anyone re-deriving it — the conclusion is unchanged, since the lock never fires either way. --- _Generated by [Claude Code](https://claude.ai/code/session_015h79niBMyoB1xcaQje3uiz)_ Co-authored-by: Claude <noreply@anthropic.com>
objectstack-ai#9702) Fixes objectstack-ai#9573 Clause-②: no `scripts/check-handler-key-read-sites.mjs` keyed a registration by its **raw type string**. `register()` in `@object-ui/core`'s `Registry` sets `ns:type`, and sets the bare `type` key **only** when a namespaced registration omits `skipFallback` — so a renderer registered as `{ namespace: 'view', skipFallback: true }` was judged against the arm of the bare key, the key `skipFallback` exists to stop it claiming. The census asked its question of a schema minted for a **different component**. `registrationsIn` now reads the third argument and resolves it to literals (an inline object, a same-file `const`, a spread of one — the tree uses all three: five page registrations hand over a shared `pageMeta` identifier and four spread it). `analyze` then looks the arm up by the keys the registry would resolve the registration under. ## The count moved in BOTH directions, and neither direction is a relaxation **Thirteen `KNOWN_UNDECLARED_READS` rows leave, and none by being declared.** Each was produced by a registration carrying `skipFallback: true` under a namespace — `action:button`, `action:icon`, `page:tabs`, `view:form`, `view:grid`, `view:list` — so the arm named in the row was another component's: a bullet/numbered list, a Shadcn button, the `ui:tabs` container. `@object-ui/plugin-list`'s own registration comment says it in the repo's words: "the bare `list` key belongs to the bullet/numbered list DISPLAY primitive".⚠️ **Ten of those thirteen reads did not leave the census.** The same read sites are still judged, under the registration that claims a *mirrored* key — `object-form`, `object-grid`, `list-view` — where all ten are declared runtime slots. What left is a **second, wrongly addressed scoring** of them. **A false green went with them, and the ledger could never have shown it.** `'form' FormSchema.onCancel` passed only because `FormSchema` is the `ui:form` primitive's own arm and mints its own `onCancel` for its own destructure; the two spellings coincided. A passing read leaves no row, so no count of the ledger could see that half. It is now scored once, on `object-form`. `detail::DetailSchema.onTabChange` **stays**: `register('detail', DetailView, { namespace: 'view', category: 'view' })` carries no `skipFallback`, so it does claim the bare key and `DetailSchema` IS its arm. Its disposition is still undecided and still owned by objectui#7804. ## The gate was not made to look at less The three reads that now have no arm anywhere (`action:button`, `action:icon`, `page:tabs` register nowhere else) are **not dropped**. They are reported as `UNMIRRORED-ALIAS` census rows carrying the bare arm they are not, counted in the OK line and printed by `--list` — boundary 3 of this gate made countable. Bringing them into judgement means **mirroring the namespaced key**, never declaring it on the bare arm. A registration whose `namespace` / `skipFallback` cannot be resolved to literals is **refused** rather than guessed at (scoped to types that have an arm, since the rest were never judged). That is also the floor under the keying itself: a broken resolver leaves every registration unkeyed and reds. The repository has zero today. ## Verification Gate, before and after on this branch (`node scripts/check-handler-key-read-sites.mjs`): ``` before OK 105 arm(s), 212 registration(s) (119 with an arm), 60 reachable handler read(s), 60 judged, ... 14 exempted by ledger after OK 105 arm(s), 212 registration(s) (103 keyed onto an arm), 46 reachable handler read(s), 46 judged, ... 14 read(s) under 16 namespaced-only alias(es) no mirror carries an arm for, 1 exempted by ledger ``` **Reverse verification (ablation), run from the committed state.** One line reverted — `armKey` back to the raw type string — with the drained ledger and everything else left in place. On-disk proof: the injected spelling greps 1 and the removed spelling greps 0, and the mutated blob hash differs from the `HEAD` blob (`dc8e822`); restore is `git checkout HEAD -- <path>` under a `trap`, proved by an empty `git diff HEAD`. ``` gate EXIT=1 x 13 handler key(s) a registered renderer reads are not declared by their arm: 'button'.onSuccess ... ButtonSchema (form.zod.ts) does not declare it. 'tabs'.onTabChange ... TabsSchema (layout.zod.ts) does not declare it. 'list'.onAddRecord ... ListSchema (data-display.zod.ts) does not declare it. ... 13 rows, exactly the thirteen this change drained pin EXIT=1 — 6 failed | 32 passed (38) ``` ⇒ the thirteen rows are **produced by the raw-string keying itself**. Each left this ledger with a reason drawn from its registration, which is why `Clause-②` stays `no`. Tests: `scripts/__tests__/check-handler-key-read-sites.test.ts` — **38 passed**, 7 new legs, including the firing control that the *same* registration **without** `skipFallback` claims the bare key and goes RED (one property, same literal, same file), a leg that mirroring `view:list` brings the read back into judgement on its own arm, the false-green leg, the `pageMeta` identifier/spread resolution leg and the unkeyable-refusal leg. The repository leg **names** the six aliases rather than counting them, so a collapsed census reds instead of reading as a clean tree. ## Surface note `packages/plugin-kanban/src/__tests__/handlerKeyDispositionsMeasured-7804.test.tsx` is a **third file beyond the two this task claimed**, and it is amended here rather than left red: its suite-4 CONTROL named `button::ButtonSchema.onSuccess` as the witness row, and that row is one of the thirteen. Its own comment prescribes exactly this repair — "re-derive it against `KNOWN_UNDECLARED_READS` rather than dropping the name and leaving the length check alone". The witness is now `detail::DetailSchema.onTabChange`, the one row that is not alias-shape. Held by 0 of the 13 open PRs (fully paginated, 1804 filenames, positive control `.github/workflows/ci.yml -> objectstack-ai#9584` fires). The changeset is empty-frontmatter: the gate's own verdict line demanded one because a published package's `src/` tree was touched (a test file), and nothing published moves. ## Acceptance notes - No new workflow and no new required CI context; the gate keeps its single exit-code channel. - No test skipped, disabled or quarantined. - Out of scope, noted and not filed: `registerLazy` carries the same `namespace` / `skipFallback` mechanics and this census reads only `register`, so a lazily-registered renderer's reads are outside it in both spellings. That is pre-existing, unchanged by this PR, and not a defect in the keying — filing it would need a measurement of whether any lazy registration has a mirrored arm at all. Carrier: whoever next widens what this census walks. --- _Generated by [Claude Code](https://claude.ai/code/session_015h79niBMyoB1xcaQje3uiz)_ Co-authored-by: Claude <noreply@anthropic.com>
… blocking (objectstack-ai#9707) Fixes objectstack-ai#9562 Clause-②: yes Authorised by the maintainer's ruling on the card (letter A, comment 5717182406, 2026-09-17T15:43Z, Director seat summon objectstack-ai#24). This relaxes a blocking gate, which is normally a human floor; the authority exists, is named, and moved the card `needs-user-decision` -> `pm:queue`. ## What was wrong `scripts/check-lockfile-dedupe.mjs` takes a LIVE registry reading (`pnpm dedupe --check`). objectui#9562 measured that reading disagreeing with itself: four green and one red on a byte-identical `pnpm-lock.yaml` inside ninety minutes, same blob at every ref, no lockfile edit anywhere and no registry publish that day. The red printed a confident `VERDICT not deduped` naming an `esbuild` peer split — and then told the reader to fix it *here*, by committing a dedupe, to a file every open pull request shares. The mechanism was never identified and this change does not adopt one. PR objectstack-ai#9690 took the live reading out of the required test lane. This is the remainder the maintainer ruled on: the `Lockfile Dedupe Check` context itself. ## What this does - **`scripts/check-lockfile-dedupe.mjs` gains one explicit flag, `--report-only`.** It keeps every reading exactly as it is and changes only the consequence: annotations become `::warning::`, the same reading is appended to `$GITHUB_STEP_SUMMARY`, and the process exits 0. - **`.github/workflows/lockfile-dedupe.yml` passes it.** That one line is where the ruling lands. - **The bare script keeps its 0 / 1 / 2 exit codes** for hand runs, which is what `pnpm check:lockfile-dedupe` still is. The mode is never inferred from `CI` or `GITHUB_ACTIONS` — only from its own flag. - **The finding text, in BOTH modes, now names the instrument**: a live registry reading that can disagree with itself; re-run before acting, and dedupe only when the split reproduces. The old unconditional "Fix it HERE ... by running `pnpm dedupe` and committing the lockfile" is gone from both modes — it is the sentence the card reported. ### What deliberately did NOT change - The job keeps its **name**, its **path filter** and its **`OPTIONAL_CONTEXTS` classification**, so nothing in branch protection or the merge queue moves. No new workflow, no new or renamed context. - **The relaxation is scoped to the VERDICT, not to the job.** A failed checkout, a broken `ci-setup-pnpm.sh`, or a `--self-test` that stops passing still reds this context and still stops a Dependabot auto-merge. That is why the classification stays live rather than vestigial. - **`clean` and `cannot-run` stay distinguishable.** Both exit 0 under the flag, so the exit code no longer separates them; the annotation does, and `cannot-run` carries one precisely because of that. A report-only mode that cannot say "I could not look" would be worse than the gate it replaces. - `pnpm-lock.yaml` is untouched, and the live smoke run below confirms `--check` still does not write it. ## Verification All runs under the shared verify lock, worktree `objectui-issue-9562-b`, at `4814064a1`. **Targeted tests** — `npx vitest run --project unit` over the six test files that read the workflow set (`check-lockfile-dedupe`, `dependabot-merge-gate`, `ci-cd-pipeline-doc`, `check-lockfile-integrity`, `check-merge-queue-head`, `merge-queue-reporting`): **212 passed (212)**, `VERDICT command-exit 0`. The dedupe file alone: 20 passed (16 before). `node scripts/check-lockfile-dedupe.mjs --self-test`: **32 cases pass** (20 before). **Reverse verification** — four one-off legs, each mutated on disk, proven landed by a before/after occurrence count on the probed file itself, run, then restored and proven restored by `git hash-object` against the HEAD blob (never by an exit code): | leg | mutation | expected | observed | |---|---|---|---| | 1 | workflow stops passing `--report-only` | the wiring pin reds | 1 failed / 19 passed — *passes `--report-only` ...* | | 2 | report-only `cannot-run` loses its annotation | leg E pin reds | 1 failed / 19 passed — *leg E survives the flag ...* | | 3 | "Fix it HERE" returns to the finding text | both-modes pin reds | 2 failed / 18 passed (the file pin **and** the shipped `--self-test`) | | 4 | the bare script stops reporting findings | the hand-run control reds | 2 failed / 18 passed (the control **and** the pre-existing exit-1 leg) | Baseline before the legs: 20 passed. Final state after: `git status --porcelain` empty on both files, both blob hashes equal to HEAD's. **Live smoke, both modes** (this tree is deduped today, so this exercises the green path end to end and not the red one): bare `node scripts/check-lockfile-dedupe.mjs` -> `VERDICT deduped`, exit 0; `GITHUB_STEP_SUMMARY=... node scripts/check-lockfile-dedupe.mjs --report-only` -> `VERDICT deduped`, exit 0, **0 bytes** written to the step summary (a clean tree writes nothing; the block itself is the signal). `pnpm-lock.yaml` sha256 identical before and after both runs. **Repo gates**, all exit 0: `check:control-bytes` (7812 tracked text files), `check:action-ref-convention` (121 refs / 39 workflows, control `actions/checkout` present), `check:required-check-set` (32 cases), `check:new-line-citations`, `check:shell-escape-residue`, `check:comment-mask-corpus` (5046 files, residue within the held-open ceiling), `check:test-path-roots`, `check:lockfile-integrity` (`VERDICT clean`). **Lint** — targeted rather than repo-wide, and the narrowing is measured, not assumed: `npx eslint` over the two changed JS/TS files, `--format json`, **2 files linted, 0 errors, 0 warnings**; the flat config declares no `project` / `projectService`, so type-aware linting is off and this diff cannot move the verdict on any file it does not touch. The `.yml` file is outside eslint's population. The repo-wide run is CI's. **Changeset** — the gate's own verdict line on this range: *"No source or published contract of a released package changed in this range, so no changeset is owed."* (3 files changed, 0 published source, 0 moved manifests, 0 changesets). So none is added, and no label is applied. ## Acceptance notes **The fence held — measured, not assumed.** `scripts/dependabot-merge-gate.mjs` was **not** touched, and does not need to be: `dependabot-merge-gate.test.ts` passes unchanged. Its assertions over this name are bucket membership (`OPTIONAL_CONTEXTS` yes, `NOT_A_GATE` no, `REQUIRED_CONTEXTS` no), the path-filter shape, and a reason string longer than 40 characters. **Nothing there asserts that a blocking-bucket name can actually fail**, so the partition stays honest with the classification unchanged. The dispatching seat's least-certain claim is therefore confirmed, with one correction worth recording: its entry's words *"Blocking when it runs"* are now **imprecise rather than false**. The job still blocks when it fails — a broken checkout, pnpm setup or self-test — and only the dedupe verdict stopped blocking. `lockfile-dedupe.yml`'s header now says so and points at that entry, so the next reader of either file is not misled. Correcting the sentence in the held file is left for whoever lands PR objectstack-ai#9584. Independent re-measurement of the hold, since it was offered for falsification: 11 open pull requests, fully paginated, **1795 filenames**; **0** of them hold any of the three files changed here; positive control `scripts/dependabot-merge-gate.mjs -> objectstack-ai#9584` discriminates. noted, not filed: `package.json`'s `check:lockfile-dedupe` script stays the bare form on purpose — the ruling reserves the hard verdict for hand runs — but nothing pins that intent, so a future author could add the flag there and quietly remove the only place the 0/1/2 contract is still reachable. Prospective author: whoever next edits this gate; it is a durability observation, not a defect, so it is recorded here rather than filed. --- _Generated by [Claude Code](https://claude.ai/code/session_015h79niBMyoB1xcaQje3uiz)_ Co-authored-by: Claude <noreply@anthropic.com>
…op (objectui#9700) (objectstack-ai#9728) Fixes objectstack-ai#9700 Clause-②: no ## ⭐ The fresh reading first — outcome 1, on today's `main` The card measured this gate at `bbe57fdd52`, before PR objectstack-ai#9702 rewrote the file. Re-measured on `dea17b469` (branch base), with `--list`: | registration | census rows | |---|---| | `detail` (registers `DetailView` **raw**) | 3 — `onNavigate`, `onAddComment`, `onTabChange`, all at `DetailView.tsx` | | `detail-view` (registers the **wrapper** `DetailViewRenderer`) | **0** | ⇒ **the wrapper hop is still invisible.** objectstack-ai#9702's re-keying did not touch it: that change decided *which arm* a registration is scored against, and this defect is about *whether the component is reached at all*. Gate exit 0 either way. ## The mechanism — derived, not inherited from the card The card declined to name one. Measured with `documentCarryingChildren` on the real `DetailViewRenderer` node: ``` before: ['ElementDataSourceGate'] after: ['ElementDataSourceGate', 'DetailView'] ``` Isolated on a minimal fixture, one token apart: ``` schema={bound as DetailViewSchema} -> Gate (hop NOT taken) schema={bound} -> Gate, DetailView (hop taken) schema={bound satisfies X} -> Gate (hop NOT taken) schema={bound!} -> Gate (hop NOT taken) ``` ⇒ ⛔ **not** the `hops > 4` cap, ⛔ **not** a failure to resolve the wrapper's reference, ⛔ **not** the wrapper stopping the walk. `resolveComponent` already handles the HOC spelling and the wrapper's inline body is walked. What closed the hop is a **type-only cast on the `schema=` attribute that hands the child the document**: `carriesDocument` tested the raw expression, so `{(bound) => <DetailView schema={bound as DetailViewSchema} />}` read as "not the parent's document" and `DetailView` was never enqueued. The data-source gate is imported from another package and cannot be followed, so that render-prop hop was the only way in.⚠️ objectui#9344 peeled exactly these wrappers off a READ receiver — *"a cast is erasure … a census that sees one and not the other is not describing the runtime"* — and stopped there. **The two failures are not symmetric.** A hidden read still leaves its component in the census; a hidden HOP removes the component entirely: no finding, no census row, no ledger row, and the green then reads as "no undeclared reads" over a file the gate never opened. That is the state objectui#9447 stood in, and it is why this is a card of its own. ## ① The repair `peelErasure` is extracted from `erasedReceiverName` and shared with `carriesDocument`, so a cast changes what the census READS about a node and never whether it SEES it.⚠️ It widens what the walk **sees**, never what it **follows**. The node underneath still has to be a document identifier or a spread of one, and an object literal writing its own `type` is still a NEW document — the narrowing that keeps `ViewSwitcher`'s `onViewChange` off `ObjectViewSchema`. Both directions are pinned. ## Measured delta on this tree — the class is ONE instance, not a population `--list` diff, before vs after: **46 -> 49 reachable reads**, and the three added rows are all `detail-view`, all read in `DetailView.tsx`. ⇒ every other `elementDataSourceBlock` wrapper in the repo (17 of them) already hopped fine; `detail-view` is the only one that hands its child a cast. The card asked for that enumeration and this is its answer: **no sibling sweep is owed.** ##⚠️ The one judgement call — a NEW ledger row, and why it is not the forbidden one The repair surfaces one finding: `detail-view::DetailViewSchema.onTabChange`. - It is the **same read site** as the existing `detail::DetailSchema.onTabChange` row — `DetailView.tsx`'s `(schema as any).onTabChange` — scored a second time, correctly, under the other registration that reaches that component. `register('detail-view', DetailViewRenderer, { namespace: 'plugin-detail' })` omits `skipFallback`, so it claims the bare `detail-view` key and `DetailViewSchema` IS its arm. - The number of live undeclared **read sites** did not move: one line, before and after. What moved is how many of the two registrations reaching it the gate can score — one, now both. - ⛔ It is **not** a row bought to make a widening quiet. Ledgering the *blind spot* would invert the map's meaning; this row is a defect the blind spot was **hiding**, which is what the ledger is for, and the file's own instruction text says so: *"if the fix belongs to another card, add the key to KNOWN_UNDECLARED_READS with the card that owns it."* - ⭐ And it **cannot outlive the repair**: under ablation the gate's own `staleExemptions()` reds on this exact row ("The defect the row waives is gone, so the row is now a live waiver for nothing"). The row is welded to the hop.⚠️ **The alternative was the Clause-② fork and was refused.** Declaring `onTabChange` on `DetailViewSchema` widens a published accept set ⇒ `Clause-②: yes` ⇒ not this card's to take. The disposition is objectui#7804's, open, and it is the card both rows carry. ## ② The pin that can go RED `scripts/__tests__/check-handler-key-read-sites.test.ts` gains a block that rebuilds objectui#9447's shape hop for hop: `register('detail-view', DetailViewRenderer)`, `elementDataSourceBlock(...)`, a data-source gate imported from another package, and the render-prop child handed `bound as DetailViewSchema`. - **The firing pin** — arm declares nothing, the gate must reach the component and report the key. - **The control on it** — same tree, key declared: GREEN, with `reads`/`judged` floors so the green cannot be a walk that found nothing. - ⭐ **FIRING CONTROL, one token apart** — the operand under the cast changed from the render-prop document to an unrelated local declared two lines above it. The hop must NOT be taken, paired with its lit half so the zero is a reading and not an unreached fixture. - Six erasure spellings on the hop side, and the built-document narrowing under a cast. - On the real tree: the three `detail-view` rows NAMED (an empty `detail-view` census is the blind spot restored), with the raw `detail` twin's three rows as the control — the repair only ever ADDS hops, so those must be untouched. ### Ablation — restore the pre-repair `carriesDocument` body, read the pin Mutation proved on disk (`peelErasure` call 2 -> 1, pre-fix paren recursion 0 -> 1; blob hash `fb8841ce` -> `b97508be`), restored via `git checkout HEAD --` with the hash re-compared and `git diff HEAD` empty. ``` gate exit=1 (staleExemptions reds on the new ledger row) vitest exit=1 11 failed | 38 passed (49) ``` All ten wrapper legs RED, plus the ledger-honesty leg. On the repaired tree: **49 passed (49)**. ## Verification | run | result | |---|---| | `pnpm --filter @object-ui/types test` | 203 files / 4757 tests passed | | `pnpm --filter @object-ui/plugin-detail test` + the gate suite | `VERDICT command-exit 0` — 181/1742 and 1/49 | | `pnpm type-check:scripts` | exit 0 (the edited test file is in that project — `--listFiles` hit 1) | | `check:handler-key-reads` | `OK … 49 reachable … 2 exempted` | | `check:control-bytes` · `check:new-line-citations` · `check:changeset-no-major` · `check:changeset-claims` · `check:pending-changeset-literals` · `check:comment-mask-corpus` · `check:component-surface-parity` · `check:element-data-source-declaration` · `check:spec-symbols` · `check:sdui-registration-pins` · `check:registry-bare-names` | all exit 0 except `check:sdui-registration-pins` | | `check:changeset-presence` | exit 0 — 1 empty-frontmatter changeset | | eslint, targeted | 4 files, 0 errors, 0 warnings | -⚠️ `check:sdui-registration-pins` exits **2 = PREREQUISITE NOT MET**, not red: *"No console build to weigh at apps/console/dist/assets."* Read as **NOT MEASURED**; this diff adds and removes no registration and does not touch the bundle. Left to CI, which builds the console. -⚠️ `pnpm -s check:changeset-no-major` reported exit 254 with no output; run directly the script exits **0** with `✅ No changeset declares a 'major' bump.` The 254 is pnpm's wrapper, not a gate. - The eslint run is a **declared narrowing**: ① the population is eslint's own config; ② 4 files, read from `--format json`; ③ invariance — `eslint.config.js` configures no type-aware linting (no `project` / `projectService`), so this diff cannot move the verdict on any untouched file. Repo-wide lint is CI's run. ## Prose this change falsified, repaired in place Two live docblocks stated *"its transitive hop stops at the wrapper"* and told readers ⛔ not to read the gate's green as evidence — `DetailViewSchema`'s `onNavigate` member and `detail-view-handler-slots-9447.test.tsx`. Both are now false in the direction that matters (they say the gate is blind where it is not), so both are rewritten.⚠️ **Comment-only**: no zod member, no runtime behaviour, no published contract field. Surface re-measured — all four files held by **0 of 13** open PRs, 1804 filenames fully paginated, positive control `.github/workflows/ci.yml -> objectstack-ai#9584`. The changeset is therefore an **empty-frontmatter declaration**, which `check-changeset-presence.mjs` names as a first-class pass. ## Acceptance notes -⚠️ `resolveComponent`'s CallExpression branch returns the first capitalised identifier argument and **discards the wrapper call's own body**, so a wrapper spelled `block(Inner)` that also renders document-carrying JSX around `Inner` would lose the outer half. Not reachable on this tree — the one such registration (`register('record_picker', elementDataSourceBlock(ElementRecordPickerRenderer))`) renders nothing around it. Noted, not filed: no undeclared read is exposed and no open PR passes this file. -⚠️ `check:sdui-registration-pins` cannot be run without a console build; nothing here is implicated. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_015h79niBMyoB1xcaQje3uiz --- _Generated by [Claude Code](https://claude.ai/code/session_015h79niBMyoB1xcaQje3uiz)_ Co-authored-by: Claude <noreply@anthropic.com>
…f moves (objectstack-ai#9744) Fixes objectstack-ai#9509 Clause-②: no — this change makes a gate see MORE. Nothing is relaxed: the went-false half keeps its exclusion of a change's own changesets verbatim, exit stays 0 on findings, and no context becomes required. ## What was actually wrong, and where `scripts/check-changeset-claims.mjs` printed, in its own output, that a **born-false** claim was outside it entirely. objectui#9509 carded three same-day instances across two pull requests. I resolved all three against the merged trees before designing anything, and **the card names the symptom's location, not the defect's**: | # | carried in | why the existing instruments cannot see it | |---|---|---| | 1 | objectui#9496's **pull request body** | not a file — no tree-scanning gate has it in its population | | 2 | objectui#9496's **pull request body** | same | | 3 | objectui#9495's `app.ts` docblock | carries **no line address at all** — it is an ordinal claim ("a grep finds that member first") | ⇒ `scripts/check-new-cross-file-line-citations.mjs` would have caught **0 of 3**, measured rather than assumed: its population is `changedPaths()`, i.e. tracked files, and its five syntaxes all require a line address. Measured directly: the five files objectui#9496 changed carry the *repaired, sha-bound* spellings and **not** the born-false ones — the false prose was never in the tree. ⇒ **the citation gate is the wrong home**, which is the seat's own least-certain claim, falsified. `check-changeset-claims.mjs` is the right one for a reason that is mechanical rather than thematic: it is the only gate here that already runs on `pull_request` **and already delivers its finding onto the pull request**, so the corpus where two of three instances live is reachable from it with ⛔ no new workflow, ⛔ no new required context, ⛔ no new permission and ⛔ no API call — `GITHUB_EVENT_PATH` is a file on the runner, read the way `check-governed-queue-guard.mjs` already reads it. ## The mechanism - **Corpus** — the prose this change publishes *about itself*: the pull request body, plus the `.changeset/*.md` bodies this change adds. ⛔ Not the tree at large; that population has a reader already. - **Coordinate** — a backticked line address resolving unambiguously to one tracked file this change touches. Same resolution rule as the existing half. - **Question** — arithmetic, never semantic. `MOVED`: this diff's own hunks map the cited base line elsewhere, or delete it. `UNANCHORED`: the file is one this change *creates*, so the number can only have come from a tree that exists nowhere outside this pull request — instance 1's shape, where the frame moved twice between revisions of one branch. - **Carve-out** — an address whose own **sentence** names the tree it was read from is never reported.⚠️ Per sentence, ⛔ not per paragraph: objectui#9496's section 2 paragraph *does* name a sha, so a paragraph-wide window would have exempted the exact claim the card is about. - ⛔ **The ask is never "correct the number."** Correcting one produces a claim true today and born false on the next insertion — the card states that before anything else. The gate asks for the durable form both pull requests converged on independently. ## Firing control, taken from the probed artefact The controls run objectui#9496's real geometry — a 28-line insertion after old line 220 and a rewrite of old line 223, as `git diff` reports it on the merged commit. **That geometry is attested outside this branch**: objectui#9496's own body and the docblock it landed both publish the figure `:246`@`b8a006883d` = `:274`@head. A mapper that drifts by one fails the control rather than reporting a clean branch, and the number cannot be re-derived to match a wrong implementation. Five controls, both directions — ⛔ a suite that only ever fires proves nothing: ``` PASS unbound-address-into-a-line-this-diff-moves: :223 -> moved, :246 -> moved (:274) PASS the-same-address-bound-to-a-sha-is-silent: (silent) PASS an-address-this-diff-does-not-move-is-silent: (silent) PASS the-insertion-point-itself-does-not-move: (silent) PASS an-address-into-a-file-this-change-adds-is-unanchored: ...:281 -> unanchored ``` A control failure exits 1 in a gate that is otherwise report-only, and says why: a differential reader that reports zero because its differ broke is indistinguishable from prose with nothing wrong in it. Pinned as failable — a judge that lies fails the suite instead of passing it. ## Run against the real artefact The reader, pointed at objectui#9496's **merged** body and its own diff (stable across both the pull request's merge base and the squash parent): **10 addresses read · 7 reported · 3 silent.** The 3 silent are exactly the section-2 and pins-list repairs three review rounds produced — the discrimination. ⭐ Of the 7, **six are confirmed born false by byte-level content comparison in both trees**, i.e. they are live instances that survived three review rounds *including a by-hand audit of all 16 citations in that body*: - the section-1 evidence table says `imported-defaults.ts:221-228` is the `tuple` arm and that `:223` is byte-for-byte `const rest = def.rest ? walk(def.rest) : undefined;`. At the merged head those lines are **the 28-line comment block the same diff inserted**; the arm is at `:249` and `:223` was rewritten. This is instance 2's exact shape, in the one section the rounds never bound. - `registry-meta-carry-9102.test.ts:833:7` — base `:833` is the assertion frame; that same line is `:885` at the head. - likewise `:163`, `:295`, `:857:7`.⚠️ Those six confirmations are **mine, by hand**. The gate asserts none of them: it reports that a number was read from a tree this change replaced and asks for it to be bound. The seventh (`:966`) I could not confirm either way and do not claim. ## Ablation — proven on disk, ⛔ never by an exit code Mutating the insertion-point boundary (`line <= hunk.oldStart` to `line <`): | leg | blob | result | |---|---|---| | at HEAD | `c4915154fdf3cb9455ad5c4f2f2763948d8cbc90` | gate exit 0, 5/5 controls, 73/73 pins green | | mutated | `dccab53d35a1db4dcd45593913be81f558006937` | gate **exit 1**, control `the-insertion-point-itself-does-not-move` FAILS reporting `:220 -> moved (:248)`, **13 pins red** | | restored | `c4915154fdf3cb9455ad5c4f2f2763948d8cbc90` | byte-identical to the HEAD blob, **and `git diff HEAD` empty** | ⭐ **The first ablation of this change changed the change.** Before the fifth control existed, that same mutation turned a unit pin red while **all four** of the gate's own controls stayed green — the gate would have printed "instrument fine" while silently reporting every stable citation at an insertion point as moved. The boundary control exists because the ablation found that, ⛔ not because it was anticipated.⚠️ And a count lesson, paid in this branch: `grep -c 'line <= hunk.oldStart'` reads **1 then 2** across the landing, and neither number is about the code — the second carrier is the comment explaining the ablation. The restore is proven by the blob hash and the empty diff, ⛔ not by that count. ## Verification - `scripts/__tests__/check-changeset-claims.test.ts` + `scripts/__tests__/render-changeset-claims-comment.test.ts` — **101 passed**. - every other test naming a file this branch touches (`check-changeset-presence`, `check-pre-install-import-graph`, `ci-cd-pipeline-doc`, `merge-queue-reporting`) — **192 passed**. - `check-control-bytes` exit 0 over 7828 tracked text files; an independent control-byte scan of the five changed files finds none. - `check-action-ref-convention`, `check-lint-coverage`, `check-node-esm-load`, `check-pre-install-import-graph` — exit 0. - `eslint . --no-inline-config` over its own full population, **5050 files**, at this head: 95 errors / 13207 warnings, **0 of them in the files this branch touches** (targeted run over those four files: 0/0). The tree-wide totals are the pre-existing state, ⛔ not a reading about this change. -⚠️ `check-required-check-set` exit 2 (HTTP 401 for the rulesets API) and `check-governed-queue-guard` exit 1 (no event payload locally) are **PREREQUISITE NOT MET**, ⛔ not findings — recorded as NOT MEASURED. `check-changeset-presence.mjs` verdict on this diff, verbatim: *"No source or published contract of a released package changed in this range, so no changeset is owed."* ⇒ no changeset. ## Surface — declared wider than dispatched, and measured The dispatch named two files. A gate's implementation and the tests asserting on its output are one surface, and **delivery is part of that output**: a born-false-only run must create the comment, or the one half nothing later will ever turn red lands in the job log objectui#9140 measured at zero answers out of four. So five files: | file | held by | |---|---| | `scripts/check-changeset-claims.mjs` | 0 of 12 open PRs | | `scripts/__tests__/check-changeset-claims.test.ts` | 0 of 12 | | `scripts/render-changeset-claims-comment.mjs` | 0 of 12 | | `scripts/__tests__/render-changeset-claims-comment.test.ts` | 0 of 12 | | `.github/workflows/changeset-presence.yml` (one expression in an existing job) | 0 of 12 | Census: all 12 open pull requests, `GET /pulls/{n}/files` fully paginated, **1807 filenames** (floor asserted — a zero-length census aborts). Positive control: `.github/workflows/ci.yml`, `lint.yml` and `scripts/dependabot-merge-gate.mjs` all resolve to **objectstack-ai#9584**, so the membership test discriminates. ⛔ No governed surface is touched: none of the five paths matches `GOVERNED_SURFACES`. ## ⛔ What this does not do - ⛔ It does not read instance 3's shape. An ordinal claim with no coordinate needs the gate to decide what a sentence means, which is the one question triage fenced off when the gate was built. Stated as a limit in the gate's own output, where the old limit used to be. - ⛔ It does not judge truth. Every finding is a request to bind a number, on a channel that still exits 0. - ⛔ It does not see a body edited without a push; `pull_request` does not fire on that. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_015h79niBMyoB1xcaQje3uiz --- _Generated by [Claude Code](https://claude.ai/code/session_015h79niBMyoB1xcaQje3uiz)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
… pronoun blindness (objectui#9727) (objectstack-ai#9752) Fixes objectstack-ai#9727 Clause-②: no The first step on this card is not repairing 53 changeset entries. It is rebuilding the instrument, because both tables the card carries are transcribed and the filing seat marked them NOT MEASURED: the producing script was cleaned up with its author's worktree, so no number on that card is re-derivable. **This pull request edits no changeset body.** It adds a measurement instrument, its pins, and one adjudication. ## The rebuilt instrument `pnpm census:changeset-polarity` (`scripts/changeset-polarity-census.mjs`), report-only, wired into no workflow — `census:*` is this tree's spelling for runnable, reported, not blocking. **P, the population.** Every sentence of every pending `.changeset/*.md` except `README.md`, matched when the sentence names a schema symbol and carries a present-tense declaration verb. Bare past `declared` is excluded on purpose: a historical sentence cannot rot, which is the distinction objectui#9713 turns on. Present-tense passive (`is declared`) is included, because it is a live claim and rots like the active voice. **V, the verdict.** For each (schema, key) pair a matched assertion predicates, membership is resolved against that schema's member set and compared with the sentence's polarity. A positive claim is contradicted when the key is absent; a negative claim when it is present. Four properties the rebuild has that the transcribed instrument did not: 1. **Whole-file, whitespace-tolerant read.** This corpus wraps near eighty columns, so a line-anchored probe is structurally blind to any claim that wraps — and a claim naming a schema *and* a key is long enough that most of them do. Lines are joined per paragraph, continuation prefixes (blockquote markers, list bullets, table pipes) stripped first, all whitespace runs collapsed before a sentence is cut. Backticked spans are masked during the cut so `foo.md` never splits a sentence. 2. **Assertion position distinguished from quoted position.** A count over prose is *not* invariant under quotation, and treating it as invariant is how a repaired site gets re-flagged forever. objectui#9713's repair quotes, in guillemets, the very sentence it retires. A sentence reached through a fence, a blockquote or quotation marks is counted, reported, and never flagged. Measured on this tree: that repaired 8802 sentence is the corpus's **one** quoted match, and it draws **zero** flags. 3. **A name is not a key — a key is (interface, name).** Membership is built with the TypeScript parser over `packages/types/src`, across both published faces (the `interface` and its zod mirror), with `extends` resolved transitively. The zod walk is **structural**: only the literal passed to `z.object()` / `.extend()`, the arms of a union, and the base of a `.merge()` / `.and()` count. A schema named in a member's *value* (`columns: z.array(ObjectKanbanLaneSchema)`) is a different object — walking into it read a lane's `cards` as a member of the *board*, which is the over-approximation this rule forbids, and fixing it is what removed those flags. An inherited index signature (`[key: string]: any`) is deliberately **not** membership: a key riding it is admitted and never examined, which is the condition the whole family of cards is about. 4. **The cross-sentence pronoun** — below. ## The blindness closed, and its pin The transcribed instrument missed **the site that motivated the card**. The claim in `.changeset/8802-8257-8008-kanban-gantt-family-retirement.md` reads, across a sentence boundary: > ... the only one that refused `allowCollapse` / `cardTemplates` / `columnWidths` / `titleField` / `draggable` / `onColumnAdd` / `onCardAdd` by name, and — through `columns: KanbanColumn[]` — the only one that judged a lane's `cards`. **The surviving `ObjectKanbanSchema` face declares none of them.** The object is `them`. A matcher that reads only the sentence carrying the verb finds a schema, finds no key of its own, and reports nothing. An instrument blind to the shape that produced it reports a **floor**, not a census. The rebuild resolves a pronoun object (`them`, `these`, `those`, `none of them`, `either`, `both`) from the nearest preceding sentence in the same paragraph that names any key. The pin lives in `scripts/__tests__/changeset-polarity-census.test.ts` and carries the **pre-repair** text of that site as a fixture, so it survives a shallow clone where `adf581278` is unreachable. ⭐ **The pin is stronger than "found".** Run against that pre-repair text, the rebuilt instrument returns exactly `columns`, `cardTitle`, `titleField`, `allowCollapse` — the four keys objectui#9713 adjudicated, and no others. The other keys that sentence names are genuinely absent from the surviving face, so a negative claim about them is *true* and is correctly not flagged. Reproducing a known-good human adjudication from a fixture is the strongest available evidence that the rebuild reads correctly. Both halves are asserted: the flags are found, and every one of them is reached `viaPronoun`. ## The re-derived count, against the card's 154 Read these from a run, not from this paragraph (commandment objectstack-ai#9). At `1cfdff814` plus this branch, `pnpm census:changeset-polarity` prints: | | card (transcribed, `dea17b469`) | this rebuild | |---|---|---| | entries scanned | 1642 | 1654 | | matched, assertion position | 199 across 169 | 218 across 176 | | matched, quoted position | not distinguished | 1 (reported, never flagged) | | object resolved across a sentence boundary | 0 — the blindness | 9 | | **candidate contradictions** | **154 across 53** | **134 across 58** | | claims naming a schema this tree does not declare | not reported | 61 | Controls, same corpus and same instrument, on every run — the run exits 2 and voids its own numbers if either fails: | control | probe | reading | expected | |---|---|---|---| | corpus lit | present-tense declaration verb | 931 | > 0 | | corpus absent | `zzqqNoSuchMemberZZ` | 0 | 0 | | member lit | `BaseSchema.type` | 1 | 1 | | member absent | `BaseSchema.zzqqNoSuchMemberZZ` | 0 | 0 | **It differs from 154, and it differs in both directions.** Flags are **down** (154 → 134) while entries are **up** (53 → 58), which is not a contradiction — they move for different reasons: - **Down**, from precision. A backticked PascalCase token is a *type* name (`ObjectGridComponentProps`, `GanttConfig`), never a key, and reading one as a key inflated the count. The structural zod walk stopped a referenced schema's keys counting as the referrer's. And a sentence in quoted position is no longer flagged at all. - **Up**, from sensitivity. The whole-file whitespace-tolerant read and the pronoun resolution both find sites a line-anchored, sentence-local matcher could not see — nine flags on this tree are reached across a sentence boundary alone. The corpus also grew by 12 entries between the two readings. ⛔ 154 was never reproduced by construction, and the rebuild was not tuned toward it. The two numbers are readings of different instruments over different trees, and the card's own statement stands: its number was a floor. ## The ten adjudications Sample rule, fixed before looking: **the first ten distinct flagged entries in sorted filename order**. No cherry-picking. Seventeen flags across those ten entries. For each, the first question is whether the flag is a **true positive** at all. Only a true positive can be ROTTED or BORN FALSE — a sentence that is *true today* is neither, and saying otherwise would be a false record of its own. Write-time refs are given for all ten so any reader can re-derive; all ten lie inside this clone's shallow window (boundary `c35fed098`, 2026-08-07), and every reading here is an affirmative one, so no negative leg needs a control. | # | entry | flags | verdict | established at | |---|---|---|---|---| | 1 | `5632-svg-host-dom-passthrough.md` | `SpinnerSchema.icon`, `SpinnerSchema.color` | **TRUE POSITIVE → BORN FALSE** | write-time ref `e304a4ef7` | | 2 | `6011-tosortitems-order-spelling.md` | `SortUISchema.toSortItems`, `.direction` | false positive ×2 | `c5fbe0b99`; true at `1cfdff814` | | 3 | `6051-gantt-flat-config-declared-keys.md` | `ObjectGanttSchema.startDateField`, `.endDateField`, `.titleField` | false positive ×3 | `75bd83d6d`; true at `1cfdff814` | | 4 | `6067-component-meta-derive-from-canonical.md` | `ComponentMetaSchema.tier`, `.namespace`, `.skipFallback`, `.labelling` | false positive ×4 | `44d075ba0`; true at `1cfdff814` | | 5 | `6121-retire-report-data-source.md` | `ReportComponentSchema.data` | false positive | `6414dfd45`; true at `1cfdff814` | | 6 | `6132-undeclared-action-props.md` | `DropdownMenuSchema.onOpenChange` | false positive | `e28fbf92f`; true at `1cfdff814` | | 7 | `6150-undeclared-but-consumed-keys.md` | `TreeViewSchema.value` | false positive | `2c45966ff`; true at `1cfdff814` | | 8 | `6169-chatbot-authoring-face-type.md` | `ChatbotSchema.chatbot` | false positive | `52a43ded8`; true at `1cfdff814` | | 9 | `6235-mergedsort-wrap.md` | `ObjectGridSchema.sort` | false positive | `9caa7d474`; true at `1cfdff814` | | 10 | `6247-option-visiblewhen-metadata-admin.md` | `SelectOptionSchema.unrecognized_keys` | false positive | `0ea559e7c`; true at `1cfdff814` | **Entry 1, the one true positive, in full.** The sentence is: > `IconSchema` and `SpinnerSchema` declare only `icon` / `size` / `color`, and both renderers already consume all three by name, so the SDUI pass-through list withholds nothing they need. On `main` at `1cfdff814`, `SpinnerSchema` declares `type` and `size`, plus `body` / `children` as retirement tombstones. It declares neither `icon` nor `color` — those are `IconSchema`'s. The sentence is false of `SpinnerSchema`. **The verdict is BORN FALSE, and it is established at the write-time ref, not today.** At `e304a4ef7` (2026-09-03), the commit that added this entry, `SpinnerSchema` was exactly `{ type, size }`. Nothing later falsified the sentence; it was false the day it was written. Re-derived mechanically by running the rebuilt census with `--corpus` and `--types` both taken from that ref — two flags, `SpinnerSchema.icon` and `SpinnerSchema.color`, with all four controls passing at that ref.⚠️ That historical run is also what found a defect **in this instrument**, and it is recorded because it is this card's own class wearing the instrument's clothes. The member lit control was first spelled `ObjectKanbanSchema.groupBy` — a member objectui#7322 added. At any ref older than that card the control read 0, the run exited 2, and every historical reading voided itself — and a historical reading is *exactly* what establishing BORN FALSE requires. **A control pinned to a fact a later card moves is the defect this card is about.** It is now `BaseSchema.type`, the protocol's own recursion point, and the choice is pinned with its reason. **The nine false positives, and the three limits they name.** All are recorded in the script header so the next reader does not re-derive them: - **Window pairing** (entries 3, 4, 7, 8, 9, 10). The sentence names schema S and key K and predicates K of something else: a registry-local type (`ComponentMeta` in `Registry.ts`, objectstack-ai#4), another node's schema (`TextSchema.value`, objectstack-ai#7; the `list-view` node's `sort`, objectstack-ai#9), a spec schema (`GanttConfigSchema`, objectstack-ai#3), a registration name rather than a key (`chatbot`, objectstack-ai#8), or a zod issue code rather than a key at all (`unrecognized_keys`, objectstack-ai#10). This is the limit the card's own author stated first and it is still the largest source. - **Polarity by keyword** (entries 3, 5, 6, 9). Polarity is read from negation words anywhere in the sentence, so an unrelated clause inverts the verdict. Entry 6 is the clearest: *"None of the three menu schemas declares any event slot (`DropdownMenuSchema` declares `onOpenChange` and nothing else)"* — the parenthetical asserts exactly what the member set says, and `None` flipped it. - **Top-level membership only** (entry 2). `SortUISchema` declares `sort?: Array<{ field; direction }>`, so `direction` is a member of the inline sort *item*, not of the schema — and *"`direction` is the key `SortUISchema` legitimately declares"* is true in context. Descending into inline member objects would re-create the "a name is not a key" failure one level down, so the narrow reading is deliberate and the cost is named rather than paid silently. ⛔ None of the three is a reason to stop reporting a flag. They are the reason a flag is a **candidate**: each is resolved by a human reading the sentence, and none is resolvable by reading the count. ## Recommendation on the grade condition Triage's checkable condition was: true-positive rate materially non-zero ⇒ defect list, p2 confirmed, splittable; ≈ 0 ⇒ re-shape the card into "fix the instrument". **Measured: 1 of 10 entries, 2 of 17 flags.** That is not zero — there is a real, previously unrecorded BORN FALSE claim in the sample, and it is a *second* instance of a class objectui#9713 had already established twice. So the existence question stays answered and `priority:p2` is right. But it is nowhere near a defect list, and **⛔ the number must stop being carried as one**. At the measured yield, splitting the 58 flagged entries into dispatchable items would dispatch roughly fifty no-ops, each costing a history read — which is the same measurement error in a new coat. ⇒ **Re-shape the card to "fix the instrument, then triage its output".** Concretely: the instrument now exists, is re-derivable from a clean checkout, and its three false-positive sources are named and individually attackable — polarity-by-keyword is the cheapest and would clear four of the nine. The 154 (now 134) should be carried as *"whatever `pnpm census:changeset-polarity` prints"*, never as a figure in prose, which is commandment objectstack-ai#9 applied to this card. The one true positive found here is a candidate for its own small card; ⛔ it is **not** repaired in this pull request, because the fence on this dispatch is absolute. ## Boundaries honoured - ⛔ **No changeset body was edited** — not one of the 53, not any other. The only `.changeset/` file this branch touches is the one it adds. -⚠️ `.changeset/6051-gantt-flat-config-declared-keys.md` is in the candidate set and is adjudicated above as a **reading only**. It is being repaired right now by objectui#9743 on another branch, and nothing here touches it. (`.changeset/5903-objectgantt-declared-keys.md` left the candidate set entirely once PascalCase tokens stopped reading as keys.) - ⛔ No published face, mirror, declaration or accept set moved. No issues created. No labels written. - Open-PR intersection re-taken on this branch rather than inherited: nine open pull requests, of which four touch `scripts/` (objectui#9748, objectstack-ai#9584, objectstack-ai#9488, objectstack-ai#8941) and four touch `.changeset/` (objectstack-ai#9540, objectstack-ai#9391, objectstack-ai#8941, and the release PR objectstack-ai#5400). None touches any path this branch adds or modifies. The one shared file is `scripts/markdown-test-inputs.mjs`, which this branch adds one ledger row to and no open pull request edits. ## The changeset grade — measured, not assumed `.changeset/9727-changeset-polarity-instrument-rebuild.md` carries **empty frontmatter**, which is the explicit claim that nothing published moved. That claim is a gate reading: - **absent control / the real diff** — `node scripts/check-changeset-presence.mjs` on this branch: *"10 file(s) changed, 0 of them published source of a package the release covers, 0 of them a manifest whose published contract moved"*, exit **0**. - **lit control, same tree, same gate** — appending one comment line to a file under `packages/types/src` flips the same gate to exit **1** and demands an entry. Reverted in the same run and proven byte-identical by `git hash-object` before and after (`bcebba9b9` both sides), with `git diff HEAD` empty. Two zeros on one instrument would mean a broken instrument. This is one zero against a firing control. ## Gates Exit codes captured to disk before any pipe. | gate | exit | |---|---| | `node scripts/check-changeset-presence.mjs` | 0 | | `node scripts/check-changeset-overwrite.mjs` | 0 | | `pnpm changeset:check` | 0 | | `pnpm check:changeset-claims` | 0 | | `pnpm check:pending-changeset-literals` | 0 | | `pnpm check:control-bytes` | 0 | | `pnpm check:governed-queue-guard` | 0 | | `pnpm check:new-line-citations` | 0 | | `pnpm check:test-path-roots` | 0 | | `pnpm check:required-check-set` | 0 | | `pnpm lint:coverage` | 0 | | `pnpm check:lint-rule-coverage` | 0 | | `pnpm check:esm-specifiers` | 0 | | `pnpm check:node-esm-load` | 0 | | `pnpm type-check:scripts` | 0 | | `pnpm exec eslint` on the three added source files | 0, three files linted | | `pnpm exec vitest run` — the new suite plus `markdown-test-inputs`, `check-changeset-presence`, `check-changeset-claims` | 0 | The governed-surface guard was run at `scripts/check-governed-queue-guard.mjs`, not under `scripts/pm/`. ## The declared file surface, and the two increments beyond it The dispatch declared "a script under `scripts/` plus its test, and a new `.changeset/9727-*.md`". Two files land outside that, both required, both named here rather than left unexplained: - **`package.json` (+1 line)** — the `census:changeset-polarity` entry. Without it the instrument has no invocation the tree knows about, and `census:*` is the spelling that carries "runnable, reported, not blocking". It is a `scripts` key: not one of the eight publish-contract fields, and the root manifest is private, which is part of why the changeset grade measures as nothing-published. - **`scripts/markdown-test-inputs.mjs` (+10 lines)** — one ledger row. That ledger asserts every tracked markdown document is an input to some test, and the six fixture documents this change adds would otherwise be uncovered and red it. The row declares what the new test reads and that it walks a markdown tree; the scanner re-derives the same answer from the source, which is why the test spells its fixture paths as literal segment runs. Both are additive and neither is touched by any open pull request. ## The fixture names ARE the trap list They encode the failure modes this lane actually hit, not a happy path: | fixture | the trap it holds open | |---|---| | `01-pronoun-pre-repair.md` | the proven false negative — a claim whose object is a cross-sentence pronoun, carried at its pre-repair text | | `02-quotation-position.md` | a count is **not** invariant under quotation; a repaired sentence quoted in its own retirement note must never be re-flagged | | `03-wrapped-assertion.md` | prose wraps near eighty columns, and it carries its own **dark leg**: the pin asserts a line-anchored probe reads nothing on that exact file, so the lit reading proves the tolerance rather than assuming it | | `04-past-tense-only.md` | a historical sentence cannot rot and stays out of the population | | `05-referenced-schema-scoping.md` | a name is not a key — a referenced schema's members are its own | | `06-index-signature-is-not-membership.md` | a key riding `[key: string]: any` is admitted and never examined; counting it as declared erases the finding | ## ⭐ A hand-typed entry guard, caught by CI and fixed — the same defect, in this instrument The first push carried a hand-typed `process.argv[1]` entry guard, and `check:entry-guard` failed the Lint job on it. **It is this script's own defect class.** Node resolves symlinks for the module graph but leaves `process.argv[1]` as the caller typed it, so a census reached through a symlink compares two different paths, answers false, does nothing, and exits **0 with no output** — a clean-looking zero from an instrument that never ran, inside an instrument whose entire purpose is to stop false zeros being reported as measurements. A wrapper holding only `result.status` cannot tell that apart from a pass. It now routes through the one predicate, `isEntrypoint` from `scripts/invoked-as.mjs`, with the reason recorded at the guard. ⛔ `KNOWN_HAND_TYPED_GUARDS` was not touched — it is shrink-only, and adding a baseline line would have been the false record rather than the fix. | | exit | |---|---| | `node scripts/check-entry-guard.mjs` **before** | **1**, naming `scripts/changeset-polarity-census.mjs` | | `node scripts/check-entry-guard.mjs` **after** | **0** — 103 scripts, 0 hand-typed guards | | `node scripts/check-entry-guard.mjs --self-test` | 0 — 63 cases | | `node scripts/invoked-as.mjs --self-test` | 0 — 12 cases, real symlinks | | census re-run after the fix | 0, controls PASS, reading unchanged | | the instrument's own suite re-run | 0 — 47 pins | The remaining Lint-job guard steps were run locally rather than eslint alone: `check-lint-coverage`, `check-entry-guard`, `check-upstream-port-parity`, `check-bash32-floor`, `check-vi-mock-override-shape`, `check-test-path-roots`, `check-cross-repo-closer-outcome` — all exit 0.⚠️ One gate is **NOT MEASURED** here and is not this change's: `pnpm check:node-esm-load` (its own workflow, not Lint) exits 1 in this container because turbo shares one cache across every worktree of a checkout and two entries — `@object-ui/auth`, `@object-ui/react-runtime` — were replayed from a sibling agent's worktree. The gate refuses to grade artifacts this tree did not produce, which is correct behaviour; the refusal is about the shared cache, not about this diff, which touches no package source. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01UanLVj6xvbS6puBCewLr8L --- _Generated by [Claude Code](https://claude.ai/code)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
…onto the shared mask, and a new one is now visible (objectstack-ai#9761) Fixes objectstack-ai#9751 Two deliverables, as triage split them in comment `5724128778`: the **residue** (four private comment strippers move onto the shared reader) and the **recurrence channel** (a new one is now visible). The second is the load-bearing half. --- ## ⛔⛔ First, the fence question, answered before anything was built **Does the instrument add a NEW CI CONTEXT? No.** `scripts/__tests__/` sits inside the `unit` project the root `vitest.config.mts` points at `scripts/**/*.test.ts`, and `ci.yml`'s `Test (shard N/4)` job runs `pnpm test --shard=N/4` — so a new `check-*.mjs` driven by a `scripts/__tests__/*.test.ts` rides a job that already exists. **No workflow file is touched**, so nothing here is fenced by PR objectstack-ai#9584 (`ci.yml`, `lint.yml`, `dependabot-merge-gate.mjs`). That claim is pinned rather than asserted: the new test fails if anything under `.github/workflows` ever names this gate, and fails if the root config stops pointing at `scripts/**/*.test.ts`. **And no `pm:retriage`.** The escalation triage reserved is for "must touch governed text, or must add another repo-level gate this repo's non-proliferation stance cannot absorb". `node scripts/check-governed-queue-guard.mjs --test` over this diff prints `NOT GOVERNED — 7 path(s) checked against 5 governed surface(s); none matched`. And AGENTS.md carries no blanket stance against a new gate — its one refusal in this neighbourhood is specific and named ("⛔ 别据此写一个「注册表 vs `main`」的交叉校验门禁 ——「应该在册」没有可靠定义"), a refusal grounded in that predicate having no reliable definition. This one's predicate is two mechanical shapes, both stated and both pinned from each side. --- ## (1) The residue — the projection is picked PER SITE | site | was | now | why | |---|---|---|---| | `overlay-node-slot-doc-types-7082.test.ts` | JSDoc-only pair | `stripComments` | reader returns member NAMES — neither a line nor an offset | | `alert-dialog-read-dialect-7104.test.ts` | JSDoc-only pair | `stripComments` | same reader | | `overlay-trigger-union-7081.test.ts` | JSDoc-only pair | `stripComments` | same reader | | `LineItemsPanel.parentIdNoCast-9333.test.ts` | blanking hybrid | `maskComments` | see below | ### ⭐ The card's claim I was asked to test rather than obey: is any of these legitimately JSDoc-scoped? **None of the three is.** The evidence is in the sites themselves, not in taste: 1. **Each already strips `//` line comments too.** A reader that genuinely wanted *JSDoc* would not carry a second rule for a form that is not JSDoc. The pair's own intent is "remove prose", and matching `/**` only is an under-approximation of it. 2. **The stated reason generalises.** `overlay-node-slot-doc-types-7082`'s own docblock said it strips first because "a `@example` fence inside one holds lines that look exactly like member rows". An ordinary `/* … */` block inside an interface body carries that hazard identically — and is invisible to the pair that was there. 3. **Nothing downstream reads JSDoc as data.** No `@param`, no tag, no docblock count. The comment is discarded, never consumed — so "is this span a comment" is exactly the question being asked, which is the question the shared reader answers. A site where the shared reader *would* be the wrong answer looks different, and this tree has one: `skill-guide-data-table-binding.test.tsx` declines it in as many words because its subject is **jsonc**. Subject language, not comment flavour, is what makes the refusal legitimate. (One `DEBT` entry below is refused on exactly that ground.) ###⚠️ A refinement to the card on the fourth site The card reads site 4 as a hand-rolled `maskComments` because it "blanks rather than deletes … preserving byte offsets". **It blanks block comments and DELETES line comments** — measured: the private projection returns 10,376 characters where the source has 15,901, so the offsets its shape implied were already gone. It is a hybrid, not a masker. It still takes `maskComments`, for reasons the card did not give: blanking is the module's safer default, it is the name the site's own function already carried, and the cost that argues for `stripComments` — a lazy `[\s\S]*?` walking the whitespace a blank leaves behind, the 6.4s→5m27s the module header records — does not apply to a census pattern with no lazy quantifier. **This is the first version of that reader for which "preserves byte offsets" is true.** ### ⛔ No damage is claimed, and here is the population that was checked Per-character comment classification, private projection versus the shared scanner, over the subjects these readers actually open: | subject | size | hand says comment / shared says code | shared says comment / hand says code | |---|---|---|---| | `packages/types/src/overlay.ts` | 52,424 chars | 0 | 0 | | `packages/types/src/feedback.ts` | 30,426 chars | 0 | 0 | | `packages/plugin-form/src/LineItemsPanel.tsx` | 15,901 chars | 0 | 0 | And at the level the readers actually report at — **26 interface bodies** (84,113 bytes) extracted by the three readers from their six doc pages and two declaration files: **0 differing member maps**. The card measured two of the four subjects; this extends it to every subject all four open, plus the doc-fence side the card did not reach. **No live mis-mask was found.** If one had been, it would be at the top of this body. ⭐ **The published numbers reproduce once you measure the quantity they name.** "52,424 bytes" and "15,901 bytes" are `String.length`, not UTF-8 bytes — those files are 52,724 and 15,955 bytes. Read as byte counts they look wrong by 300 and 54; read as the character counts the instrument actually produces they are exact. --- ## (2) The recurrence channel — `scripts/check-hand-rolled-comment-mask.mjs` ### The shape it refuses A regex literal that **both** 1. **spans a comment** — an escaped block opener, a wildcard repetition, an escaped block closer; or an escaped line opener followed by a to-end-of-line consumer; **and** 2. **sits in a removal position** — first argument of `.replace(`, or immediately preceded by `!` (the rejection half of a line filter). Both halves were measured, not reasoned. Dropping (1) reports `/^[ \t]*(?:\/\*\*?|\*\/|\*|\/\/)[ \t]?/`, which trims comment *markers* off text already known to be a comment — a different job the shared masker does not do. Dropping (2) reports `ownHeaderComment`, which uses the very shape in (1) with `.exec` to *find* this file's leading docblock. The tree holds both, and neither is this defect. The detector reads the tree through `js-comment-mask.mjs` itself — **both** flag arrays. The `literal` array is what tells a real regex literal from a carrier shape quoted inside a string, a distinction a regex cannot make: `literal` covers content and not delimiters, so a regex literal's opening `/` is unflagged while the same character inside a string is flagged. **There is no exemption list at all** — not for the gate, not for its test, not for `js-comment-mask.mjs`. The shapes in both new files are assembled from fragments instead. What it **cannot** see is in its header rather than left to be discovered: a hand-written scanner loop, a `.split()` on a comment regex, a rejection routed through a named `const`. ### ⭐ Which shape, and why — the argument triage deliberately did not make **A shrink-only `DEBT` ratchet**, the shape `check-lint-coverage.mjs` and `check-type-check-coverage.mjs` already use here. Enforced in **both** directions: | condition | exit | |---|---| | a carrier at a path `DEBT` does not name | `1` BREACH | | a `DEBT` entry whose site no longer carries one | `2` STALE — delete the line | | corpus below `CORPUS_FLOOR` | `3` REFUSAL — nothing was read, never a pass | The second row is the whole difference between a ratchet and the growing baseline triage forbade. An allowlist is paid for by **adding** a line; this one is paid off by **deleting** one, and a fixed site that keeps its line turns the tree red. A BREACH also outranks a STALE entry, so a new carrier cannot be laundered by fixing an old one — pinned from both sides. **Zero was not available, and that is a measurement rather than a preference.** Run over `2414e3751` — this branch's base, before the four conversions — the sweep reports: ``` swept 5060; carriers 14 in 9 file(s) … 4 files NOT IN DEBT … verdict code 1 (private comment projection outside DEBT); breaches 4; stale 0 ``` and the four it names are **exactly** the four objectstack-ai#9751 was scoped to convert, no others. **The card's population of four was short by five.** Those five are the `DEBT` this lands with: | entry | verdict | |---|---| | `packages/components/…/empty-base-classes-override-friendly-8525.test.tsx` | ⛔ **not payable by conversion** — subject is CSS; this masker is graded against a *JavaScript* parser, and its narrowness is a decision, not a gap | | `packages/i18n/…/console-namespace-3546.test.tsx` | in class, convertible; outside this card's declared file surface | | `scripts/check-doc-example-shared-reader.mjs` | in class, convertible — and the only one that is a **live gate**, where a phantom comment makes two different expressions read as equal | | `scripts/__tests__/console-vite-alias-closure-4925.test.ts` | in class, line-filter shape; the rewrite is a filter, not a one-line swap | | `scripts/__tests__/vitest-config-alias-targets-3944.test.ts` | same shape, same rewrite | ⛔ **None of the card's four is in `DEBT`** — the constraint "do not turn the four known sites into a growing baseline" is met by the four being *gone*, not excused. The five are a worklist and are reported for follow-up cards rather than taken here; four of them sit outside what this card claimed, and the fifth (`check-doc-example-shared-reader.mjs`) is a live gate whose conversion deserves its own subject-by-subject measurement rather than a ride on this diff. --- ## Verification ⛔ **Everything that triggers a turbo build went through `/home/user/objectstack/scripts/pm/os-verify-lock.sh`**, and each run's `VERDICT command-exit` line is the reading quoted. | what | how | result | |---|---|---| | the four converted sites + the new gate test | `vitest run` over the five files | 5 files, **179 tests pass** | | `packages/types` type-check | **through the lock** | `VERDICT command-exit 0` | | `packages/plugin-form` type-check | **through the lock**, after `pnpm --workspace-concurrency=2 --filter '@object-ui/plugin-form^...' build` (also locked) | `VERDICT command-exit 0` | | `pnpm type-check:scripts` | **through the lock** | `VERDICT command-exit 0`; `--listFiles` confirms both new files are inside it | | `check:node-esm-load` | **through the lock**, the run that card named above all | `VERDICT command-exit 0`; provenance leg **37 of 37 built by this tree**, load leg 34 of 39.⚠️ The first attempt exited **1**, and the cause was environmental rather than this diff: turbo shares one cache across every worktree of a checkout, and it replayed `@object-ui/auth` and `@object-ui/react-runtime` from a **parallel agent's tree** (`objectui-issue-9509`). The gate refuses to grade artifacts this tree did not produce — that refusal is the gate working. `--force-build` cleared it. | | `check:comment-mask-corpus` | population-coupled — reads every JS source, including both new files | exit 0; 5062 files, 1 disagree, **1517 over-masked bytes — unchanged**, the objectui#7882 residue | | `check:control-bytes` · `check:entry-guard` · `check:test-path-roots` · `check:new-line-citations` · `check:pending-changeset-literals` · `check:changeset-claims` · `check:shell-escape-residue` · `check:doc-example-readers` | population-coupled, run before pushing | all exit 0 | | `one-authority-per-exported-name-6273` | the new module exports names a sibling also exports | 11 tests pass | | `check-changeset-presence` | | exit 0 — empty frontmatter, declared as releasing nothing | **ESLint, narrowed and declared.** `eslint --no-inline-config --format json` over the 6 changed files: **0 errors**; 2 warnings, both `no-explicit-any` on a pre-existing interface this diff does not touch. Three pieces of evidence for the narrowing, because a count alone is not a verdict: the population is `eslint.config.js`'s own; the file count (6) is read from the JSON reporter; and the invariance is structural — `eslint.config.js` declares no `project` / `projectService`, so linting here is **not type-aware** and nothing in this diff can move the verdict on a file it did not touch. The repo-wide run is CI's. ### ⭐ A bare count is never a verdict — the tokens that must NOT move - **`1517` over-masked bytes in `check:comment-mask-corpus`.** It must not move because that number is objectui#7882's residue in `apps/console/src/pages/DocsIndex.tsx` — a file this diff does not touch, and a defect this diff does not repair. A change in it would mean the sweep's *subject* moved, not that this diff improved anything. It reads `1517` before and after. - **The member maps of all 26 interface bodies.** They must not move because the conversion is a change of *reader*, not of *subject*: every assertion in the three files is about names and type texts the declarations still spell identically. 0 differ. - **`0` FABRICATES bytes.** The direction the module's header calls worse than no verifier at all is asserted at zero by the corpus gate, and stays zero. ###⚠️ The ablation came back in a direction the template does not have, and it is reported as measured The obvious reverse verification for half (1) — replace the shared reader with an identity function and watch the test redden — **came back GREEN**, and that is a finding rather than a flaw in the recipe. The mutation is proved to have reached disk (anchor occurrences 1 to 0, mutant 0 to 1, blob `0d3d86d5c` to `a79d4b89a`), it was restored to the byte (blob back to `0d3d86d5c`, `git diff HEAD` empty), and the run is reproducible without vitest at all: > **16 interface bodies compared with the projection and with no projection whatsoever: 0 member maps move.** ⇒ **The comment projection at these three readers is doubly latent today.** The card measured that the private and shared projections agree; this measures the stronger thing — that on today's subjects, *no* projection produces the same answer. The `@example`-fence hazard the original docblock named is real and a doc page can regain one, so the guard is not pointless; but it is a guard against a shape the subjects do not currently carry. Two consequences, both stated rather than buried. **Nothing is at risk from this conversion** — that is the strongest safety statement available, and it is measured. And **these tests cannot validate the conversion**; its correctness rests on the classification measurement above (0 disagreeing characters over three files, 0 differing member maps over 26 bodies), which is the instrument that can actually see it. The same is true of site 4 from the other direction: its census pattern finds 0 matches in `LineItemsPanel.tsx` masked, unmasked, stripped or blanked. ### ⭐ The new pin can fail for the reason it names Shown two ways, not inferred from a green run: 1. **The gate itself, unchanged, over the pre-fix tree** (a `git worktree` at `2414e3751`): **exit 1**, naming the four carriers and no others. That is the red half of red-then-green, taken with the shipped instrument rather than a mutation of it. 2. **A NEW carrier, introduced into the live tree.** A one-function probe at `scripts/zz-9751-ablation-probe.mjs` — a path `DEBT` does not name — turned the shipped gate **red on the spot**: `swept 5065 source file(s); 8 private comment projection(s) in 6 file(s)`, `BREACH scripts/zz-9751-ablation-probe.mjs`, **exit 1**. Removing the probe returns it to exit 0 and `no carrier outside DEBT`. That is the card's entire scenario — "a new one appeared" — driven end to end, and the worktree is verified clean afterwards. 3. **`judge()` driven directly** at each boundary: a carrier outside `DEBT` → `EXIT_BREACH`; a `DEBT` entry with no carrier → `EXIT_STALE`; both at once → BREACH wins; a corpus below the floor → `EXIT_REFUSED`; an unknown argument → `EXIT_USAGE`, read as a **number** from a spawned process rather than by matching prose. Every subprocess in the new test carries an explicit generous timeout (`300_000`), so a slow box cannot redden it for a reason it does not name. ###⚠️ Proving the search could find something before believing it found nothing Every regex in this work lives in a `.mjs` file, never inside a shell quote. The first-pass detector was run over the whole tree and **returned 27 hits in 14 files** before either predicate was narrowed; the narrowing is what took it to 14 in 9, and each shape dropped along the way is a `CLEAN` case in the self-test. A detector that had come back empty would have been treated as broken, not as good news. --- ## Acceptance notes - **Out of scope, noted, not filed:** `scripts/check-doc-example-shared-reader.mjs`'s `canonical()` is the one `DEBT` entry that is a live gate rather than a test, and its failure direction is the fabricating one (two different expressions reading as equal through a phantom comment). It sits inside this card's declared `scripts/` area, and converting it was still declined: it is a gate with its own subjects and its own regression surface, and it belongs in a change that measures them. Whoever picks it up inherits a `DEBT` line that names it. - The two `scripts/__tests__/` line-filter carriers are a different rewrite (a filter, not a one-line swap) and are likewise left to the ratchet. - `packages/i18n/…/console-namespace-3546.test.tsx` is outside the claimed file surface and was not touched. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_015h79niBMyoB1xcaQje3uiz --- _Generated by [Claude Code](https://claude.ai/code)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
…rsession (objectstack-ai#18859) Fixes objectstack-ai#18828 Clause-②: no The claim protocol forbids a second `Claim:` — the rule is this file's own, in the objectstack-ai#17366 docblock at `scripts/pm/check-clause2-carriers.mjs:378` — and until this PR nothing READ it. A thread that carried the forbidden second line was RANKED, not refused: the governing-claim selector took the newest live claim that parses a branch and printed the loser as `rejected: 1 … a SUPERSEDED claim`, clean and green at exit 0, in the register reserved for a transition the protocol DESIGNED. A writer-side prohibition with no enforcing reader. `claimRepeats` and the C8 row are that reader.⚠️ **This is a RESUMED delivery.** Three commits were already on this branch from a run whose container was killed in its final-gates phase. Nothing it wrote was rewritten and nothing was redone — every item of the dispatch contract was re-verified against the tree, and all three readings were re-taken at the current tip and are dated below. The verified/fixed ledger is the first section. ## What was VERIFIED and what was FIXED Every contract item was found already correct and is left **byte-unchanged**. No code fix was needed; the only commit this run adds is a merge of `origin/main` (the derivation was answering about a stale tree — see Gates). | contract item | finding | |:---|:---| | the reading is a VERDICT at `EXIT_PAIR_ADVERSE` (4), never a NOTE at 0 | **verified** — `C8` is pushed in `pairRows` (:4430); `pairNotes` does not carry it, pinned | | `claimCarrierSelection` stays a pure function of the rows it is handed | **verified** — byte-identical to `origin/main` (sha256 of the whole function `40f59ba86082c358` at both revs) | | `CLAIM_COMMENT_MARKER` unwidened | **verified** — it is *imported* from `check-half-states.mjs` and read through `markerMatches`; that file is not in this diff at all | | `CLAIM_SELECTION_RULE` and the governing-claim choice unchanged | **verified** — byte-identical across revs; every hunk but two is a pure insertion | | SUPERSEDED / RETRACTED wordings byte-unchanged where they still apply | **verified** — no hunk touches them; the fixture control below prints the SUPERSEDED sentence identically at both revs | | every pin (a)–(i) present, each with a non-vacuity control | **verified** — 52 `t(...)` cases in the battery block; the ablation shows all nine directions carried | | battery registered in the roster with its case count | **verified** — :792, pinned at 52, and the block declares exactly 52 | | `SELF_TEST_BATTERY_FLOOR` raised by exactly one | **verified** — 31 → 32 (:806) | | the live census extended for this shape, population named | **verified** — the five measured instances replayed from their real rows, with `objectstack-ai#18559` as the sanctioned-shape control | `rowAuthor`, `laterOnThread` and `claimRetractions` are byte-identical across the two revs as well. ## The before-reading — the fixture control through the exported reader The same two rows (one author, two claims each parsing a branch, no retraction between) through the same exported `claimCarrierSelection` / `pairInputRecord` / `pairRows`, at `origin/main` `88aa326deb` and at this branch: | | `origin/main` `88aa326deb` | this branch | |:---|:---|:---| | `claims` / `live` / `pool` / `rejected` | 2 / 2 / 1 / 1 | 2 / 2 / 1 / 1 — **unmoved** | | governing claim | `7100000002` | `7100000002` — **unmoved** | | `rejected[0].reason` | `a SUPERSEDED claim — it is not the newest LIVE claim that parses a branch …` | byte-identical | | `claimRepeats` exported | **no — the reader does not exist at that rev** | yes | | `claim.repeat` in the record | absent | `1 author(s) holding more than one LIVE claim comment …` | | `pairRows` codes | *(none)* | `C8` | | **verdict** | **exit 0 — nothing refused** | **exit 4 (`EXIT_PAIR_ADVERSE`)** | That is the defect and the repair in one table: the selector does not move, and the thread stops reading green. ## The live count — the five cards and the control, re-taken 2026-09-18T00:44:39Z Read per card through the gate's own `markerMatches` / `CLAIM_COMMENT_MARKER` and `claimRetractions`, not by eye.⚠️ Those threads may have left this state since. | card | `Claim:` comments (author) | `Clause-②-correction:` | `Release:` | the OLD reading | C8 today | card state | open delivering PR | |:---|:---|:---|:---|:---|:---|:---|:---| | objectstack-ai#18540 | 2 — `os-support-ai` (5719079496, 5720020876) | 0 | 0 | SUPERSEDED, exit 0 | **named, exit 4** | closed | **none** | | objectstack-ai#18677 | 2 — `os-support-ai` (5720104138, 5720190458) | 0 | 0 | SUPERSEDED, exit 0 | **named, exit 4** | closed | **none** | | objectstack-ai#18748 | 2 — `os-support-ai` (5720212595, 5720888122) | 0 | 0 | SUPERSEDED, exit 0 | **named, exit 4** | closed | **none** | | objectstack-ai#18651 | 2 — `os-support-ai` (5721424769, 5721997887) | 0 | 0 | SUPERSEDED, exit 0 | **named, exit 4** | closed | **none** | | objectstack-ai#18778 | 2 — `os-support-ai` (5721425530, 5722028692) | 0 | 0 | SUPERSEDED, exit 0 | **named, exit 4** | closed | **none** | | objectstack-ai#18559 *(control)* | 1 — `os-support-ai` (5721425131) | **1** (5721779100) | 0 | *(nothing rejected)* | silent | closed | **none** | The card's table reproduces exactly. All six cards are now **closed**, and the open-PR column is empty for every one of them: the only open PR cross-referenced from any of these threads is objectstack-ai#18857, whose body's closing keyword names `objectstack-ai#18780` instead — `prDeliversCard` answers `false` for all six and `true` for `objectstack-ai#18780` (the control leg), so it is not paired with any of them. ⛔ The repair of the five is `os-support-ai`'s; this PR only names them, and posts nothing on those cards. ## The escalation probe — the triage's p1 condition, MEASURED The triage marked this p2 because all five instances were one seat self-superseding, and named the escalation condition it had not run: a second `Claim:` from a DIFFERENT session on one card, which would be a silent ownership transfer printed green. I ran it. **Population, read 2026-09-18T00:47:37Z → 00:48:42Z:** every open card on both boards this gate reads — **529 open cards in `objectstack-ai/objectstack`, 413 in `objectstack-ai/objectui` (942 total)**, of which **880** carry at least one comment and were read; **163** carry at least one claim comment. 9 comment lists sit at the 100-comment cap and are UNJUDGED past it, exactly as objectstack-ai#18683 prescribes. 0 parse failures. **The answer is not 0 — it is 12.** Twelve open cards carry LIVE `Claim:` comments from two or more DIFFERENT authors with no retraction between them: | repo | card | authors holding live claims | |:---|:---|:---| | objectstack | `objectstack-ai#13503` | `claude[bot]` + `baozhoutao` | | objectstack | `#14026` | `hotlong` + `claude[bot]` | | objectstack | `objectstack-ai#15811` | `os-bill` + `os-litant` | | objectstack | `objectstack-ai#17852` | `os-warren` + `os-litant` | | objectui | `objectstack-ai#4730` | `yinlianghui` + `os-sales` | | objectui | `objectstack-ai#7070` | `os-warren` + `claude[bot]` | | objectui | `objectstack-ai#7696` | `os-justin` + `os-tesla` | | objectui | `objectstack-ai#7804` | `os-tesla` + `os-sam` + `os-justin` | | objectui | `objectstack-ai#7848` | `claude[bot]` + `baozhoutao` | | objectui | `objectstack-ai#7924` | `os-warren` + `os-sales` | | objectui | `objectstack-ai#8115` | `claude[bot]` + `yinlianghui` | | objectui | `objectstack-ai#9370` | `os-tesla` + `os-justin` |⚠️ **This PR is deliberately SILENT on all twelve** — and that silence is pinned, per direction (b). Refusing an ownership transfer between sessions is not this card's to do: it is a different state, it would need its own remedy sentence, and a row that answered both would make one sentence out of two states. This is reported here and in the dispatch report so the seat can file it; ⛔ it is not folded in. ## Who the new exit 4 meets before it lands The seat needs this before landing, so I swept it rather than assuming. Two facts: 1. **No CI job turns red.** `check:pm-clause2-carriers` — the only wiring, `.github/workflows/lint.yml:1140` — runs `--self-test` and nothing else. No workflow runs `--pair` or a sweep, so landing this changes no required context. The exit 4 appears only when a seat runs `--pair` or a sweep by hand. 2. **On the objectstack board: nobody.** Of 32 open PRs in objectstack, **none** delivers a card that would newly earn a C8. Twenty open cards across both boards would earn the row (report-only, listed in the dispatch report), but only one is reachable through an open PR, and it is in the sibling repo: **`objectstack-ai/objectui#9584`** (open, not draft; its closing keyword names `objectui#9499`), which delivers a card carrying two live claims by `os-try-charles` (5663366106 on 2026-09-14, 5690579598 on 2026-09-16). That pair answers exit 4 at its next `--pair`. `DEFAULT_SWEEP_REPO` is `objectstack-ai/objectstack`, so objectui is only ever read when passed explicitly. ⛔ Nothing was posted on objectstack-ai#9499, objectstack-ai#9584 or any of the twelve. ## The reading, and WHERE it is computed `claimRepeats` (:1898) is a **sibling pure reader beside `claimRetractions`, built on it** — the same map decides membership here and for governance, so the pool and this row cannot describe two different retractions. It names every author holding more than one LIVE claim comment, orders them by the file's one recency rule (`laterOnThread`, to ORDER the record, never to pick a winner), and resolves no state, no row and no exit code. `c8SecondClaimSameSeat` (:4380) renders the verdict; `pairRows` (:4430) pushes it as row `C8`; `pairInputRecord` adds `claim.repeat` (:5876, declared in `INPUT_RECORD_PAIR_FIELDS` at :5641) as the READING — one derivation feeding both, so the record and the verdict cannot disagree about how many claims a seat holds or which they are. **MEMBERSHIP first, and that is what makes the state repairable.** The state is read over LIVE claims only. A re-claim after a `Release:` is the protocol working and reads exactly as it did before. And a seat that already wrote a second claim has an act that clears the row: `Release:` what it holds, then one fresh `Claim:`. A rule written over the writing *moment* instead ("no retraction strictly BETWEEN the two lines") would have been unrepairable by construction — nothing un-writes a comment — so the row would have been a permanent red with a remedy nobody could execute. ### The four axes - **实际业务需求** — measured, not assumed. Five live instances on the objectstack board at filing, re-confirmed today, every one of them read green before this row; plus 20 open cards across both boards that carry the state now. The first signal in five occurrences came from a dev reading a docblock, not from any instrument. This is a real shape occurring repeatedly, not a speculative surface. - **项目长远合理性** — contract-first, and no workaround. The rule already existed in writing at :378; this adds the reader that enforces it, in the same file, over the same thread, through the same membership derivation governance uses. No new exit code was minted, no second selector, no second reader of the marker. The prohibition and its reader now live one screen apart. - **防 AI 写代码犯错** — this is the axis that decides the exit. A second `Claim:` re-enters the pool as the newest claim and becomes what every downstream reader is handed — the property the correction key was deliberately designed NOT to have. Rendering that as a NOTE at exit 0 is precisely the tolerant-consumer shape this repo refuses: an adverse fact printed green is how a batch of identical mistakes stays invisible. Declaring the prohibition and not enforcing it is the "声明而未兑现" gap; the repair is to enforce it loudly, at `EXIT_PAIR_ADVERSE`. The row also ⛔ never prescribes WHICH repair — choosing between a correction and a release would be choosing whether the card is being re-taken, which is the seat's judgement, so it names both and writes nothing. - **创业阶段不扩散需求** — the surface added is one file, one pure reader, one row, one record field. It refuses exactly one shape the protocol already forbade in writing and re-blocks no legal workflow: a card claimed once reads as it always did, a re-claim after a `Release:` reads as it always did, a `Clause-②-correction:` is not a claim and never was. The cross-seat question, which is a genuine second capability, is explicitly NOT taken here. ## The pins — per direction, each with a non-vacuity control | | direction | reading | |:---|:---|:---| | (a) | same author, two claims, no retraction | **named, exit 4**; the row carries both ids, the author and both repairs | | (b) | DIFFERENT authors | supersession as today, exit 0 — ⛔ not this state | | (c) | same author after a `Release:` **or** the id-naming retraction | RETRACTED as today, exit 0; the `⛔ NOT superseded` wording byte-unchanged | | (d) | a `Clause-②-correction:` as the later row | silent; the objectstack-ai#17366 exit still reads the declaration off it | | (e) | a DECORATED second claim (bold, backticked) | counted through `markerMatches` exactly as a bare one; the raw constant refuses both, so the counting is the sibling's ONE reading | | (f) | a second claim whose `Branch:` parses to zero branches | still named — the prohibition is on the WRITING, not the parse | | (g) | three claims by one seat | **ONE** refusal naming all three, not two | | (h) | an unattributable row (`rowAuthor` null) | fail closed, as `claimRetractions` does — and `null` never groups with `null` | | (i) | a later same-author comment that QUOTES or DISCUSSES the word | silent — the marker is read at line start | Direction (i) has a control in the wild on this very card: the triage comment 5722477144 contains the word `Claim:` mid-line, and `markerMatches` refuses it — card objectstack-ai#18828 reads one claim comment, so `--pair` on this PR is silent. **Roster line** (:792): `'objectstack-ai#18828: a SECOND \`Claim:\` by ONE seat — the writer-side prohibition, finally READ': 52` — and the battery block declares exactly 52 `t(...)` cases. **Floor** (:806): `SELF_TEST_BATTERY_FLOOR` **31 → 32**, raised by exactly one. ## The ablation Run from the **committed** fix, twice, each leg proving its mutation landed on disk before the reading was taken and proving its restore by an empty `git diff HEAD` and by blob hash — never by an editing command's exit code. `HEAD` blob `3a270ef2eb5f33780e04e4732714f8e88d74a017`. | leg | mutation | mutated blob | result | |:---|:---|:---|:---| | baseline | none | `3a270ef2eb…` | **941 cases pass, exit 0** | | **A** — the repeat detection neutered (a group is never reported) | `72115d2796ead200f93aa855c8ba5820a83f5f8f` | | **23 of 941 failed**, exit 1 | | **B** — the SAME-AUTHOR check removed (the author no longer decides the grouping) | `40cfadd4f5740f34210675ceb998fb2977823769` | | **3 of 941 failed**, exit 1 | Both legs restored: `git diff HEAD` empty, blob back to `3a270ef2eb…`. **Total case count is 941 in all three runs** — the rest of the self-test is byte-identical in its case count, and in both legs **0 of the failures fall outside the objectstack-ai#18828 battery**. Leg A is the interesting one, because it shows the per-direction controls doing their job. Five of the nine directions assert SILENCE and therefore *cannot* go red when the detection is removed — their non-vacuity controls go red instead. All nine directions are carried: - pin itself red: **(a) (e) (f) (g)** - carried by its control: **(b) (c) (d) (h) (i)** — "make those two authors ONE", "drop the retraction", "write that same correction as a SECOND `Claim:`", "give that same row a login", "move that same word to the OPENING of a line" Leg B is the narrower, sharper one: removing only the author test reds **3** cases, and pin (b) is among them. That is the pin which distinguishes this card from the cross-seat question — proof the author test is load-bearing and that (b) is not vacuous. **Self-test count: 889 before → 941 after** (+52, exactly the registered battery). The 889 was measured by running `--self-test` in a detached worktree at `origin/main` `88aa326deb`. ## Gates Derived from the worktree with `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` (no hand-fed path list).⚠️ The first derivation printed **STALE TREE** — the branch was 2 commits behind `origin/main` and 8 files the derivation reads had changed — so `origin/main` was merged in first and the list re-derived on the merged tree at `7424cf3f44`; the `--repo` assertion holds against this checkout's `origin`. **34 derived, 34 run, all exit 0.** Each exit code captured redirect-then-`$?`, never across a pipe. ``` 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 ``` Reconciled with `--ran`, exit codes included: **34 derived, 34 run, 0 NOT-MEASURED, 0 UNRUN** — "a DERIVED zero — all 34 recorded an exit code and none of them is 3". Repo-wide `pnpm lint` (`eslint . --no-inline-config`): **exit 0**. The heavy run took a ticket through `scripts/pm/os-verify-lock.sh` (slot `issue-18828-dev`), queued behind the seat's own `dispatch-gates.mjs --self-test`. `node scripts/pm/check-clause2-carriers.mjs --pair` on this PR is reported in the dispatch report — this card carries one claim comment, so the row is silent on it. `skip-changeset`: `scripts/pm/**` publishes nothing from any released package — the whole diff is one non-published script. --- _Generated by [Claude Code](https://claude.ai/code/session_01Gqi43smmqjJ5sUrhfoPeKu)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
Fixes #9499
Scope is the director-seat ruling (comment
5682566291, batch #135 item 1, letter C), not the card body, which predates it. All three changes land in one PR.What changed
test-aggregate(name: Test)needs: [test, test-dist-pins],if: always() && github.event_name != 'push'. The single required test context.testpnpm test --shard=N/8.timeout-minutes: 20unchanged; no test skipped, disabled or quarantined; the relevance gate not widened.test-dist-pins(name: Test (dist pins))Run built-artifact pins (dist project)step, lifted out of the matrix, carrying thetestjob's relevance gate verbatim.⭐ The acceptance: a shard forced to
skippedturns the aggregator red — measured, not arguedneeds.test.resultalone is a phantom gate, and the measurement says so in two ways. Both readings are from a real Actions run on branchclaude/issue-9499-proto(workflowPROTO 9499), which stands in for the shard matrix with trivial jobs so the mechanism is the only thing under test.Run
35045618251, attempt 1. Read back withGET /actions/runs/{id}/attempts/1/jobsandGET /check-runs/{id}/annotations— raw job logs are served from a blob host this agent's egress is denied, so the proto emits its readings as annotations, which the REST API does serve.Test (shard 1/4),(2/4),(4/4)successTest (shard 3/4)— forced to skip withif: falseskippedPROTO allskip (…)— a 2-leg matrix whose whole job carriesif: falseskippedPROTO naive aggregator—needs: [test, test-shard-3], noalways()skippedTest— the realscripts/check-test-shard-results.mjsfailureThe gate's own annotation on that failure, verbatim:
The rollup, on the same run, verbatim:
⇒ two distinct phantom shapes, both measured on the same run:
needs.test.resultreadssuccesson a run where a job namedTest (shard 3/4)wasskipped. An aggregator that trusted the shard matrix's rollup would have been green there. The gate that reads each shard's own conclusion is red.needs:-without-always()aggregator is itselfskipped— and a skipped required context counts as success in branch protection, so that aggregator does not merely mis-report, it vanishes.test-aggregatecarriesif: always()for exactly this.matrixcontext to a job-levelif:, so a single leg cannot be skipped while its siblings succeed. The forced-skipped shard above is therefore its own job under the shard's name — which is what the gate looks at anyway, since it reads job names and conclusions, not matrix membership. The reachable shapes are the three the gate covers: the whole matrix skipped, a leg missing from the matrix, and a leg failed/cancelled/timed-out.How the gate works
scripts/check-test-shard-results.mjsreads this run's own job list fromGET /actions/runs/{id}/attempts/{n}/jobs(actions: read, nothing written) and asserts each shard by name and by its own conclusion.skippedis not a pass. It fails closed — an unreadable answer is exit 2 and a red context — and it refuses any verdict over a job list that does not contain its own job, because that endpoint answers200with an empty list for a run id that does not exist, which would otherwise make "no shards found" and "every shard was removed" the same reading.The dist-pin job is asserted straight from
needs.test-dist-pins.result: a non-matrix job's result is its own conclusion andskippedis visible in it, so no API read says anything more.The shard count is the one thing the script does not derive.
scripts/__tests__/check-test-shard-results.test.tspins the--shardsargument written inci.ymlagainstci.yml's ownshard: [...]matrix, and pins the shard-name spelling against the workflow through the same parserdependabot-merge-gate.test.tsuses. Drift is red, locally and loudly — which is the difference from the repository-settings surface the count used to be welded to.Why the ruleset rename was the blocker
Naming the legs made the shard count a member of GitHub ruleset
11776024: 4 → 8 renames four live required contexts at once, and between the rename and the settings edit every pull request blocks forever on a check that can no longer report while every queue build burns the ruleset's 60-minute status-check timeout. ⛔ This PR is not mergeable on its own — the ruling sequences three steps around it (remove the fourTest (shard n/4)contexts, merge, addTest), and that sequencing is the lane seat's and the maintainer's, not this PR's.Two consequences recorded rather than left to be discovered:
WATCHED_CONTEXTSinscripts/check-required-check-set.mjsnow names the set the ruling installs. Between this merge and the third step the live answer is the old one, so the patrol reportsdrifted— exit 0, printed in the run summary, never red. That is objectui#9422's two-tier split doing exactly what it was built for. A new test pins that reading against the committed 2026-09-14 live fixture, naming both halves (Testmissing, the four shard names unexpected).REQUIRED_CONTEXTSnamesTestinstead of the four shards; the eight shards andTest (dist pins)move toNOT_A_GATEwith the reason. They are blocking legs — the difference is that one context reports their verdict.The #4959 counterfactual, re-read
dependabot-merge-gate.test.tscarries a frozen verbatim record of the 2026-08-17 incident. It is not edited. What changed is what today's required set makes of it: the four shard names moved frompendingto onemissingname, and the case now asserts that too, so the drop from nine pending contexts to five cannot stand for a weakened case.Testas one name, or for whichever shard reported first, would have merged this pull request". That warning is about a single check produced by a shard, which reports while its siblings still run.test-aggregatehas aneeds:edge to every leg, so it cannot report before the last of them, and it is red unless each leg's own conclusion issuccess. The replacement case states that difference as behaviour, with a control in the same command: the same snapshot with the same two failed shards is green when the aggregator sayssuccess, so the red is the aggregator speaking and not a residual shard name.Local verification
Every exit code below was redirected to a file and captured before any pipe. Worktree
objectui-issue-9499, tree clean, atd3e01fe48.pnpm exec vitest run --project unit scripts/__tests__pnpm type-check:scriptspnpm check:control-bytespnpm check:required-check-setpnpm check:merge-queue-headpnpm check:entry-guardpnpm check:test-path-rootspnpm check:action-ref-conventionpnpm check:shell-escape-residuepnpm check:pre-install-import-graphpnpm lint:coveragepnpm docs:check-linkspnpm check:doc-fences·check:doc-types·check:doc-example-ids·check:spec-symbols·check:new-line-citationspnpm check:governed-queue-guardnode scripts/check-changeset-presence.mjspnpm check:doc-snippetscheck:doc-snippetsis exit 2, which its own output callsPRECONDITION NOT MET (exit 2) — The snippet program was NOT run, explicitly "not a verdict about any document". It needs a 34-package build first. Recorded as not measured rather than as a pass or a failure. The bound on what that leaves open: this change adds zero code fences to the page (git diff -- content/docs | grep -c '^+.*```'→0), andcheck:doc-fencesis green over it.changeset — the gate's own verdict line, quoted:
⛔ No
skip-changesetlabel is applied and no empty-frontmatter changeset is written:grep -rn skip-changeset .github/ scripts/outside__tests__returns zero andci-cd-pipeline-doc.test.tspins that label as a phantom.lint, narrowed and declared.
eslint --no-inline-configover the 11 changed JS/TS files (count read from--format json): exit 0, 0 errors, 0 warnings. Population: the repo-widepnpm lintscan is CI's and is not run here. Invariance: this repo's flat config sets noparserOptions.project/projectService, so no rule in it is type-aware and nothing in this diff can move the verdict on a file the diff does not touch.⛔ No build, no
domproject and no package test run: this diff touches.github/workflows/**,scripts/**andcontent/docs/**only, andscripts/is not a workspace package, so the affected-package closure is empty. Every test that reads any file in this diff lives underscripts/__tests__, and all 167 of those files ran.Acceptance notes
test-coveragedeclares its ownshard: [1, 2, 3, 4], its own--shard=N/4and its ownblob-N-4.jsonfile names; nothing is shared with thetestmatrix. Theci.yml:741sentence "sharded the same 4 ways" was a description, not a coupling, and it is now reworded to say so. No coverage-lane change is required by this card, which is the reading objectui#9271 needs.check:merge-queue-headpins no check name at all.check:required-check-setpins onlyType Check; the shard names are in the watched tier, whose absence is reported and never red — but a separate assertion ("every pinned and watched name is declared blocking inREQUIRED_CONTEXTS") does couple them, so the update was in scope after all, for a different reason than the one assumed.content/docs/guide/ci-cd-pipeline.mdis forced, and not by the add-a-workflow rule:ci-cd-pipeline-doc.test.tspins the job table againstci.yml'sjobs:keys in both directions, pins each row's Appears as against the job'sname:, and pins each row's What it runs against the job's first-party commands. Two new jobs means two new rows or a red test.matrixcontext to a job-levelif:, so the literal "mixed rollup" shape cannot be produced from a matrix. The reachable forms are: the whole matrix skipped, a leg removed from the matrix, and a leg failing or cancelled. The gate covers all three, and the proto run demonstrates the first and third.needs:edges between them", whichcoverage-report(needs: test-coverage, objectui#5403) had already falsified. Corrected here because this PR adds the second such edge and the sentence sits in the paragraph the new rows belong to. Successor: whoever next edits that section — this PR is that editor.changeset-release.yml,check-links.yml,labeler.yml) enumerate the required set as it stood, each under an explicitRe-measured … at 2026-09-06T17:20Zdateline. A dated fact cannot drift, so they are left alone. The present-tense claims about the same set —lint.yml's step comment, threescripts/__tests__headers and two paragraphs of the page — are corrected here, because this change is what made them false. Successor: none needed.🤖 Generated with Claude Code
https://claude.ai/code/session_015h79niBMyoB1xcaQje3uiz
Generated by Claude Code