Skip to content

ci: one Test aggregator becomes the required test context, shards 4 -> 8, dist pins get their own job - #9584

Merged
os-zhuang merged 5 commits into
mainfrom
claude/issue-9499-ci-test-aggregator-required-context
Sep 20, 2026
Merged

os-zhuang merged 5 commits into
mainfrom
claude/issue-9499-ci-test-aggregator-required-context

Conversation

@os-try-charles

Copy link
Copy Markdown
Collaborator

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) New. needs: [test, test-dist-pins], if: always() && github.event_name != 'push'. The single required test context.
test Matrix 4 → 8, pnpm test --shard=N/8. timeout-minutes: 20 unchanged; no test skipped, disabled or quarantined; the relevance gate not widened.
test-dist-pins (name: Test (dist pins)) New. The shard-1-only Run built-artifact pins (dist project) step, lifted out of the matrix, carrying the test job's relevance gate verbatim.

⭐ The acceptance: a shard forced to skipped turns the aggregator red — measured, not argued

needs.test.result alone is a phantom gate, and the measurement says so in two ways. Both readings are from a real Actions run on branch claude/issue-9499-proto (workflow PROTO 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 with GET /actions/runs/{id}/attempts/1/jobs and GET /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.

job conclusion
Test (shard 1/4), (2/4), (4/4) success
Test (shard 3/4) — forced to skip with if: false skipped
PROTO allskip (…) — a 2-leg matrix whose whole job carries if: false skipped
⛔ PROTO naive aggregator — needs: [test, test-shard-3], no always() skipped
⭐ Test — the real scripts/check-test-shard-results.mjs failure

The gate's own annotation on that failure, verbatim:

Test :: 1 test shard(s) did not report success: Test (shard 3/4) (skipped). ⛔ 'skipped' is not a pass here -- that is the whole reason this gate reads each shard instead of the matrix rollup (objectui#9499).
:: Process completed with exit code 3.

The rollup, on the same run, verbatim:

ROLLUP :: needs.test.result=success (3-leg matrix, every leg success) | needs.test-shard-3.result=skipped (one job, if: false) | needs.allskip.result=skipped (2-leg matrix, whole job if: false)
ROLLUP SEEN BY THE GATE :: needs.test.result=success

⇒ two distinct phantom shapes, both measured on the same run:

  1. needs.test.result reads success on a run where a job named Test (shard 3/4) was skipped. 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.
  2. ⭐ A needs:-without-always() aggregator is itself skipped — and a skipped required context counts as success in branch protection, so that aggregator does not merely mis-report, it vanishes. test-aggregate carries if: always() for exactly this.

⚠️ One shape could not be produced and is reported as such rather than asserted: GitHub does not expose the matrix context to a job-level if:, 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.mjs reads this run's own job list from GET /actions/runs/{id}/attempts/{n}/jobs (actions: read, nothing written) and asserts each shard by name and by its own conclusion. skipped is 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 answers 200 with 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 and skipped is 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.ts pins the --shards argument written in ci.yml against ci.yml's own shard: [...] matrix, and pins the shard-name spelling against the workflow through the same parser dependabot-merge-gate.test.ts uses. 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 four Test (shard n/4) contexts, merge, add Test), 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_CONTEXTS in scripts/check-required-check-set.mjs now names the set the ruling installs. Between this merge and the third step the live answer is the old one, so the patrol reports drifted — 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 (Test missing, the four shard names unexpected).
  • REQUIRED_CONTEXTS names Test instead of the four shards; the eight shards and Test (dist pins) move to NOT_A_GATE with the reason. They are blocking legs — the difference is that one context reports their verdict.

The #4959 counterfactual, re-read

dependabot-merge-gate.test.ts carries 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 from pending to one missing name, and the case now asserts that too, so the drop from nine pending contexts to five cannot stand for a weakened case.

⚠️ That file used to end a case with "a gate that waited for Test as 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-aggregate has a needs: edge to every leg, so it cannot report before the last of them, and it is red unless each leg's own conclusion is success. 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 says success, 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, at d3e01fe48.

command exit
pnpm exec vitest run --project unit scripts/__tests__ 0 — 165 files passed, 2 skipped; 4798 tests passed, 2 skipped
pnpm type-check:scripts 0
pnpm check:control-bytes 0 — 7740 tracked text files scanned
pnpm check:required-check-set 0
pnpm check:merge-queue-head 0
pnpm check:entry-guard 0
pnpm check:test-path-roots 0
pnpm check:action-ref-convention 0
pnpm check:shell-escape-residue 0
pnpm check:pre-install-import-graph 0
pnpm lint:coverage 0
pnpm docs:check-links 0
pnpm check:doc-fences · check:doc-types · check:doc-example-ids · check:spec-symbols · check:new-line-citations 0
pnpm check:governed-queue-guard 0
node scripts/check-changeset-presence.mjs 0
pnpm check:doc-snippets 2 — NOT MEASURED

check:doc-snippets is exit 2, which its own output calls PRECONDITION 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), and check:doc-fences is green over it.

changeset — the gate's own verdict line, quoted:

✅ No source or published contract of a released package changed in this range, so no changeset is owed.
Compared the working tree with f7fcc2cdb (merge-base with origin/main): 16 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 skip-changeset label is applied and no empty-frontmatter changeset is written: grep -rn skip-changeset .github/ scripts/ outside __tests__ returns zero and ci-cd-pipeline-doc.test.ts pins that label as a phantom.

lint, narrowed and declared. eslint --no-inline-config over the 11 changed JS/TS files (count read from --format json): exit 0, 0 errors, 0 warnings. Population: the repo-wide pnpm lint scan is CI's and is not run here. Invariance: this repo's flat config sets no parserOptions.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 dom project and no package test run: this diff touches .github/workflows/**, scripts/** and content/docs/** only, and scripts/ is not a workspace package, so the affected-package closure is empty. Every test that reads any file in this diff lives under scripts/__tests__, and all 167 of those files ran.

Acceptance notes

  • Zone 2 assumption 1 — the coverage lane is NOT coupled. test-coverage declares its own shard: [1, 2, 3, 4], its own --shard=N/4 and its own blob-N-4.json file names; nothing is shared with the test matrix. The ci.yml:741 sentence "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.
  • Zone 2 assumption 2 — half falsified. check:merge-queue-head pins no check name at all. check:required-check-set pins only Type 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 in REQUIRED_CONTEXTS") does couple them, so the update was in scope after all, for a different reason than the one assumed.
  • Zone 2 assumption 4 — falsified. content/docs/guide/ci-cd-pipeline.md is forced, and not by the add-a-workflow rule: ci-cd-pipeline-doc.test.ts pins the job table against ci.yml's jobs: keys in both directions, pins each row's Appears as against the job's name:, 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.
  • A matrix leg cannot be skipped while its siblings succeed. GitHub does not expose the matrix context to a job-level if:, 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.
  • noted, not filed: the page's Core CI section stated "Every job runs in parallel — there are no needs: edges between them", which coverage-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.
  • noted, not filed: three workflow comments (changeset-release.yml, check-links.yml, labeler.yml) enumerate the required set as it stood, each under an explicit Re-measured … at 2026-09-06T17:20Z dateline. A dated fact cannot drift, so they are left alone. The present-tense claims about the same set — lint.yml's step comment, three scripts/__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

… -> 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

Copy link
Copy Markdown
Collaborator Author

⛔⛔ MAINTAINER ACTION REQUIRED — this PR must NOT be enqueued in the normal way

domain:devx @ objectui seat, session_015h79niBMyoB1xcaQje3uiz, R60, 2026-09-16T02:09Z. PR is ready and all 39 checks are green — and it is deliberately not armed with auto-merge.

Why enqueuing this now would wedge the merge queue

This PR renames the four required test contexts. Per the ruling on objectui#9499 (comment 5682566291):

The old and new names are never both satisfiable, so this is the only order that does not block forever; the window between the first and third step has no test context required and should be done in one sitting.

This tree no longer produces Test (shard 1/4) … Test (shard 4/4) — it produces eight shards plus one aggregator named Test. ⇒ if the four old names are still required when this enters the queue, the queue waits on four contexts that can never report. ⛔ Not hypothetical: it is the exact failure the ruling's ordering exists to prevent.

The three steps, in order — steps 1 and 3 are the maintainer's clicks

# actor action
1 🧑 maintainer Remove the four required contexts Test (shard 1/4), Test (shard 2/4), Test (shard 3/4), Test (shard 4/4) from the ruleset
2 🤖 seat Enqueue and merge this PR (I do this once step 1 is confirmed)
3 🧑 maintainer Add Test as a required context

⚠️ Steps 1→3 should be done in one sitting: between them there is no test context required at all, which is the cost the ruling accepted.

⚠️ Expected, designed, and pinned — do not be alarmed by it: between step 2 and step 3, required-check-set-patrol.yml will report drifted (the live required set is the old one while the tree declares the new one). That is exit 0, printed to the run summary, never red, and scripts/__tests__/check-required-check-set.test.ts pins exactly that reading against the committed 2026-09-14 fixture. ⛔ After step 3, re-take the live reading — ⛔ do not edit WATCHED_CONTEXTS to agree with whatever the endpoint happens to say.

What earns the confidence — the acceptance was demonstrated, not argued

Proto run 35045618251 (instrument branch claude/issue-9499-proto, ⛔ not part of this PR; same convention as the existing claude/issue-5403-proto). Read by this seat from the API:

job conclusion
Test (shard 1/4) · (2/4) · (4/4) success
Test (shard 3/4) forced if: false skipped
Test — the new gate ⭐ failure
PROTO naive aggregator (needs: without always()) skipped

Annotations on the failing gate job, verbatim:

1 test shard(s) did not report success: Test (shard 3/4) (skipped). ⛔ 'skipped' is not a pass here
[notice] needs.test.result=success

⭐ On the very run the gate failed, the matrix rollup reads success. That is why the ruling said explicitly, and why this gate reads each shard by name instead of the rollup. Second phantom measured: an aggregator without always() is itself skipped — and a skipped required context counts as success in branch protection, so it would not mis-report, it would vanish.

Serial note

content/docs/guide/ci-cd-pipeline.md here is also touched by PR #9581 (card objectui#9140). #9581 lands first; this PR merges main before step 2. ⛔ Not handed to the queue as a conflict.

⛔ timeout-minutes: 20 is not raised · ⛔ no test skipped, disabled or quarantined · ⛔ the relevance gate is not widened.


Generated by Claude Code

This was referenced Sep 16, 2026
akarma-synetal pushed a commit to akarma-synetal/objectui that referenced this pull request Sep 17, 2026
…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>
@os-steve
os-steve enabled auto-merge September 20, 2026 14:04
@github-actions

github-actions Bot commented Sep 20, 2026 •

Copy link
Copy Markdown
Contributor

changeset-claim-re-read

⚠️ 1 address(es) in this pull request's own prose name a tree it replaced

Each was read from a tree this change itself moves, so a reader who follows it lands somewhere else. ⛔ Nothing here blocks and nothing here says the sentence is false — the question asked is arithmetic: does this diff move the line that number points at?

  • in this body, ci.yml:741 — this change moves .github/workflows/ci.yml:741 to :761

    The ci.yml:741 sentence "sharded the same 4 ways" was a description, not a coupling, and it is now reworded to say so.

⛔ The repair is not to correct the number. Changing :246 to :274 is true today and born false again on the next insertion — objectui#9509 states that before anything else. Bind the number to the tree it was read from (`:246` at `b8a006883d`, `:274` at this head), which cannot re-stale because each number names its own tree; or state a rule instead of a coordinate, the way objectui#9495 replaced a file count with "every file in git diff --name-only against the merge base".

Not covered, stated so nobody reads this as more: a born-false claim that spells no line address at all (objectui#9495 coordinated one by ORDINAL — "a grep finds that member first" — and deciding that means reading what the sentence means), a claim spelled as a symbol or a package rather than a backticked file name, and a file named ambiguously.

Compared the checked-out tree with 698090479 (merge-base with origin/main): 16 file(s) changed outside .changeset/, read against 1234 pending declaration(s) that publish a body (1799 pending in total). · run

@os-steve
os-steve disabled auto-merge September 20, 2026 14:06
@os-zhuang
os-zhuang enabled auto-merge September 20, 2026 14:25
@os-zhuang
os-zhuang added this pull request to the merge queue Sep 20, 2026
Merged via the queue into main with commit eb1c9f9 Sep 20, 2026
40 checks passed
@os-zhuang
os-zhuang deleted the claude/issue-9499-ci-test-aggregator-required-context branch September 20, 2026 14:39
akarma-synetal pushed a commit to akarma-synetal/objectui that referenced this pull request Sep 28, 2026
…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>
akarma-synetal pushed a commit to akarma-synetal/objectui that referenced this pull request Sep 28, 2026
…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>
akarma-synetal pushed a commit to akarma-synetal/objectui that referenced this pull request Sep 28, 2026
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>
akarma-synetal pushed a commit to akarma-synetal/objectui that referenced this pull request Sep 28, 2026
… 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>
akarma-synetal pushed a commit to akarma-synetal/objectui that referenced this pull request Sep 28, 2026
…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>
akarma-synetal pushed a commit to akarma-synetal/objectui that referenced this pull request Sep 28, 2026
…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>
akarma-synetal pushed a commit to akarma-synetal/objectui that referenced this pull request Sep 28, 2026
… 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>
akarma-synetal pushed a commit to akarma-synetal/objectui that referenced this pull request Sep 28, 2026
…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>
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Sep 28, 2026
…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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

4 participants