Skip to content

fix(cli): os build's text face renders every author-time advisory its summary line counts - #18857

Merged
os-support-ai merged 6 commits into
mainfrom
claude/issue-18780-build-text-face-advisory-count
Sep 18, 2026
Merged

os-support-ai merged 6 commits into
mainfrom
claude/issue-18780-build-text-face-advisory-count

Conversation

@os-support-ai

Copy link
Copy Markdown
Collaborator

Fixes #18780

Clause-②: no

os build's text face counted per-package author-time advisories it never printed: ⚠ 4 author-time warning(s) — see above standing over a list of 3. The list grows to the count.

⚠️ This is a RESUMED delivery. Two commits were already on this branch from a run whose container was killed before it opened a PR. They carried no verification — no test reading, no ablation, no changeset measurement, no gate sweep survived. They were re-read adversarially and re-measured from scratch; three defects in them are corrected in this PR and are listed below.

The card's reading, re-derived rather than relayed

The card recorded the 4-vs-3 count as the #18677 deliverer's, explicitly not re-driven by the filing seat. It reproduces exactly. Measured on examples/app-multi-package, compile.ts at this branch's merge-base ad1f94e8ec, CLI run from source, NO_COLOR=1:

os build         exit 0   3 rendered advisory entries   summary: "4 author-time warning(s) — see above"
os build --json  exit 0   warnings: 4, of which 1 carries a `package 'ID' — ` prefix
os validate      exit 0   4 rendered advisory entries   (no "see above" sentence on that face at all)

With compile.ts at this branch's HEAD, same fixture: os build renders 4, summary reads 4, --json still carries 4. The mutation was proven on disk by blob hash before each reading and the restore proven by an empty git diff HEAD — not by an editing command's exit code.

⭐ One correction to the card's framing, offered rather than assumed: os validate renders its four through a different printer — one line per advisory, no closing rule: ID at PATH line — and it has no summary sentence to keep honest. So "the two doors disagree on the rendered list" is right, but only os build can carry this defect at all. os lint cannot either: see above appears in exactly one place in packages/cli/src, and it is compile.ts.

Which side moves, and what the chosen side costs — measured

The card deliberately did not rule on which side moves. The list moves, not the count, and the cost of that is measured rather than argued:

face before after delta
single-package stack (no packages[]) 2038 bytes 2038 bytes the two clocks only — Load time: Nms, Build complete (Nms)
union-level author-time FAILURE 3590 bytes 3590 bytes Load time: Nms only
multi-package stack 3 entries under a count of 4 4 entries under a count of 4 the block renders below the Running author-time rules per package (N)... step line, and gains the per-package entry

So the only rendered byte this moves is the one the card exists to move. #18769 held os build's text output byte-identical on purpose; that hold is honoured everywhere except the defect itself.

Shrinking the count instead would have made the text face report 3 while its own --json and os validate both report 4 — the false-clean direction #11529 named one list over, and it would have needed a second binding to count a rendering rather than a set.

The pin, and the two controls

packages/cli/test/build-text-face-advisory-count.test.ts asserts an equality read from one run, not a number: the integer in the summary line, the count of entries rendered above it, and the length of --json's warnings. A fixture that raises a different number of advisories keeps passing; a face that counts a set it did not print cannot.

Both directions were run, each from a committed tree, each with the mutation proven on disk and the restore proven clean:

  • fails before — compile.ts reverted to its pre-fix blob, pin unchanged: Tests 2 failed | 3 passed, on summary said 3; rendered list: … expected 2 to be 3 and on this per-package finding rides --json and the text face never prints it.
  • passes after — at HEAD: Tests 5 passed.
  • the lit control can fail, and fails on the condition it names — strip packages[] out of the fixture and the equality assertions all stay GREEN while the fixture reaches the per-package pass and raises a survivor there goes RED: 1 failed | 4 passed. That is exactly the vacuous pass the control exists to refuse.

What was rewritten in the pre-existing diff

  1. A raw ESC byte in the pin. stripAnsi carried a literal 0x1B inside its regex literal — the class check:nul-bytes rejects, invisible in every reader. Rewritten as an escape; byte-identical at runtime.
  2. A red that predated this branch. The new once-guard is a call site in compile.ts, and validate-build-gate-parity.test.ts has held a CLOSED roster since [finding] validate-build-gate-parity.test.ts says 「There is no third option」 but its detector matches only lint*/validate* call sites — a find*-named gate wired into ONE command passes today #18491 — every bare-identifier call site must land in exactly one ledger. It was in none, so that file failed twice (every call site … is classified, and the parity gap derived from the same set). The roster and the assertion are both present at this branch's merge-base and the name is absent there, so the red was carried by the two commits this branch started from, not introduced by merging main. Classified as NOT_A_GATE under the presentation reason, with the argument written next to it: the guard decides WHEN printAuthoringAdvisories is called and nothing else, and validate.ts has nothing to wire because it has no "see above" sentence.
  3. A changeset sentence stricter than its own measurement. It claimed the single-package captures "differ only in the Build complete (Nms) timer". Re-measured: they differ in two clocks — Load time: Nms as well. Both are clocks, so the claim survives; the sentence now says what was actually measured.

What was verified and kept

  • Every exit between step 3b and the summary flushes. Enumerated: the union-failure text branch, the per-package-failure text branch, the continuing path, and the catch-all. The --json branches return early through the guard, and isExitSignal re-throws ahead of the catch's flush so no face double-prints.
  • ^ {4}rule: really does tell an advisory from an error. printAuthoringAdvisories closes each entry with a four-space line; printAuthoringRuleErrors and printDocIssueErrors both indent theirs by six.
  • The --json leg's where-typeof filter isolates ruleAdvisories exactly. None of the other five members of warningsSoFar() carries a where key — DocIssue, NavContributionGroupDiagnostic, PermissionSetNameCollisionDiagnostic, the capability-provider pairs and the plain-string undeclared-key list were each read. So the third assertion is sound in general, not only on this fixture.

Changeset level, measured on the built dist

⛔ Not inferred from files[] and not from the file's look:

  • positive control — author-time warning(s) — see above is present in 1 published file, packages/cli/dist/commands/compile.js;
  • negative control — a token nothing in the tree carries is present in 0;
  • dist/commands/compile.js sha256 4999075… with compile.ts at its pre-fix blob, f1bbb59… with the fix — published bytes move;
  • restoring and rebuilding a third time reproduces f1bbb59… exactly, which is what makes the middle reading a measurement rather than build noise.

@objectstack/cli is public and versioned and ships dist, so a changeset is owed. patch: a bug fix in a released package, Clause-②: no — no schema key, no closed-set member, no published export, no registry entry, and no payload key, exit code or --json byte moves. check-widening-tells --declaration no over this diff reports no file on any declared surface (3 NOT MEASURED, 0 tells).

Verification run on this branch

origin/main is merged in (not rebased); both pre-existing commits are still ancestors, verified by git merge-base --is-ancestor exit 0 on each — a positive reading, which is self-proving in any checkout.

  • pnpm --filter @objectstack/cli exec vitest run --project unit — 214 files, 3048 tests, 0 failed
  • pnpm --filter @objectstack/cli exec vitest run --project integration — 51 files, 427 tests, 0 failed
  • the six nightly-tier os build pins, run under OS_TEST_TIERS=nightly because the queue population excludes them by name — build-json-advisory-parity, build-json-undeclared-key-parity, build-json-failure-warnings, build-multi-package-artifact, compile-artifact-packages, build-docs-step-count: 6 files, 41 tests, 0 failed
  • pnpm --filter @objectstack/cli typecheck — exit 0. Note tsconfig.json includes src only, so the test layer is judged by the check:test-typecheck half of that script, which reports the layer compiling under tsconfig.test.json.
  • pnpm --filter '@objectstack/cli^...' build — exit 0 (the dependency closure).
  • the gate families scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack derives from this branch's own change set, each exit code captured before any pipe.

⚠️ Two earlier readings were contention artefacts and are recorded as such rather than as failures: four scaffold-emission-typechecks cases reddened in a run that was competing with a gate sweep and was then killed at a timeout, and both pass in the clean full run above.

Acceptance notes

  • check:type-check-debt was killed by the OOM killer (exit 137 inside its full-repo build, gate exit 3) during a contended sweep. Its own failure text says that is not a pass and not a finding — nothing was measured. It is re-run alone in the final sweep and the reading is in the report.
  • Noted, not filed: printDocIssueErrors and printAuthoringRuleErrors render identical six-space rule: lines, so a text-face assertion that keys on that indent alone cannot tell a doc error from a rule error. Nothing in this PR depends on it, no open PR is heading for that file, and there is no carrier — recorded here rather than as a card.

Generated by Claude Code

… summary line counts

The advisory block was printed at step 3b, before step 3b-ii appended the
per-package survivors to the same `ruleAdvisories` binding the summary line
counts. On a multi-package stack that made `N author-time warning(s) — see
above` point at a list strictly shorter than N.

Claude-Session: https://claude.ai/code/session_01DvvamiacK328idtBYJBxV3
Co-authored-by: Claude <noreply@anthropic.com>
…byte

`check:nul-bytes` rejects every raw ASCII control byte in a tracked text
file. The pin's `stripAnsi` carried a literal ESC inside its regex
literal, which renders as nothing in every reader and is precisely the
class that gate exists to reject. The escape spelling is byte-identical
at runtime.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DvvamiacK328idtBYJBxV3
…roster

`validate-build-gate-parity.test.ts` has held a CLOSED roster since #18491:
every bare-identifier call site in `compile.ts` and `validate.ts` must land
in exactly one of `SHARED_NON_REGISTRY_GATES`, `BUILD_ONLY_GATES` or
`NOT_A_GATE`. The once-guard introduced for the text-face fix was a new
call site in `compile.ts` and was classified nowhere, so the file reddened
twice — once on "every call site is classified", once on the parity gap it
derives from the same set.

Measured, not inferred: the roster and the classification assertion both
predate this branch (present at its merge-base), and the name is absent
from the roster there, so this red was carried by the two commits this
branch started from rather than introduced by merging `main`.

The guard is presentation: it decides WHEN `printAuthoringAdvisories` is
called and nothing else. Every finding it renders is produced by
`runAuthoringRules` and `runPerPackageAuthoringRules`, both already
classified as gates, and the same list rides `--json` whether it runs or
not — so it is a `NOT_A_GATE` row under the presentation reason, not a
`BUILD_ONLY_GATES` row: `validate.ts` has nothing to wire, having no "see
above" sentence to keep honest.

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DvvamiacK328idtBYJBxV3
The unchanged-face claim was measured again from a committed tree, with
compile.ts reverted to its pre-fix blob for the before leg and the restore
proven by hash. The two captures are 2038 bytes each, as recorded — but
they differ in TWO fields, not one: `Build complete (Nms)` and
`Load time: Nms`. Naming one of them made the sentence read as a stricter
measurement than the one that was taken, and a reader reproducing it would
have found a second difference the text does not account for.

Both are clocks, so the claim the bullet makes is unchanged: on a stack
with no `packages[]` this fix moves no rendered byte.

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DvvamiacK328idtBYJBxV3
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/cli, touching 1 documentable anchor(s).

17 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: node scripts/docs-audit/affected-docs.mjs --json 88aa326deb8c0599803643be885708d391ef356e.

⛔ 3 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails.

What this run could not see
  • 1 name(s) were too generic to anchor anything (single lowercase words)
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

Coarse fallback — 24 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 88aa326deb8c0599803643be885708d391ef356e → packageMentionDocs.

Which tree this was computed on

This run read content/docs from faa1a998b97663bbb972ba33829b69a1c48e62b8 — the merge of head c2562dac95ed642891f2c91c1ac02f9ec3b9479d into base 88aa326deb8c0599803643be885708d391ef356e, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin faa1a998b97663bbb972ba33829b69a1c48e62b8 && git checkout faa1a998b97663bbb972ba33829b69a1c48e62b8
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 88aa326deb8c0599803643be885708d391ef356e c2562dac95ed642891f2c91c1ac02f9ec3b9479d && git checkout -B drift-repro 88aa326deb8c0599803643be885708d391ef356e && git merge --no-ff c2562dac95ed642891f2c91c1ac02f9ec3b9479d

node scripts/docs-audit/affected-docs.mjs --json 88aa326deb8c0599803643be885708d391ef356e

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs 88aa326deb8c0599803643be885708d391ef356e → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@github-actions github-actions Bot added documentation Improvements or additions to documentation tests tooling labels Sep 18, 2026
@os-support-ai
os-support-ai marked this pull request as ready for review September 18, 2026 01:09
@os-support-ai
os-support-ai added this pull request to the merge queue Sep 18, 2026
Merged via the queue into main with commit c7dc089 Sep 18, 2026
40 checks passed
@os-support-ai
os-support-ai deleted the claude/issue-18780-build-text-face-advisory-count branch September 18, 2026 01:44
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

Labels

documentation Improvements or additions to documentation domain:cli size/m tests tooling

Projects

None yet

2 participants