Skip to content

fix(pm): the card-comment read pages to a declared cap, so a thread past 100 is UNJUDGED rather than a truncated claim pool - #18799

Merged
os-justin merged 3 commits into
mainfrom
claude/issue-18683-card-comments-page-ladder
Sep 17, 2026
Merged

os-justin merged 3 commits into
mainfrom
claude/issue-18683-card-comments-page-ladder

Conversation

@os-justin

Copy link
Copy Markdown
Collaborator

Fixes #18683

Clause-②: no

The defect

scripts/pm/check-clause2-carriers.mjs read a card's comment thread with ONE request — /issues/{n}/comments?per_page=100, no page= ladder, no short-read check — while the two sibling list reads in the same file paged to a declared cap and answered null (UNJUDGED, never clean) when they hit it. One file, two OPPOSITE defaults on "I did not read everything", and the fail-OPEN one was the read that arbitrates OWNERSHIP: the governing-claim pool, its membership, and the Clause-② declaration read out of it all come from those rows. A thread past 100 comments handed the pool its first page and nothing said the tail had been dropped, so a claim written past row 100 was not superseded — it was never a candidate — and a superseded carrier governed in its place.

The before-reading, on a 101-row fixture

Driven end to end through the real CLI against a stubbed board (--pair, no network), on main d9ba33df4c (script blob d753e2a8cf06d8f72c436e0a1917b2fecb8be813). Two fixtures, both 101 rows, both differing from a complete thread only past the page boundary.

fixture BEFORE (main) AFTER (this PR)
100 claim-free rows, the 101st the only Claim: card-comments: 100 row(s) · claim.selected: none — no comment on this thread carries a line beginning \Claim:`· **exit 4, row C2absent`** card-comments: 101 row(s) · the 101st claim is the pool · claim.clause2-line: DECLARED \no`` · exit 0
row 1 an older Claim: declaring yes, the 101st a newer one declaring no claim.clause2-line: DECLARED \yes`` from the SUPERSEDED carrier, which is not even listed as rejected · exit 4, row C3 the newer claim governs, the older is listed REJECTED/SUPERSEDED · DECLARED \no`` · exit 0

The second row is the fail-OPEN direction stated as a measurement: one thread, two readings, and they disagree on the declaration itself.

The ladder, and the cap

All three list reads now go through one pagedListRead helper — it pages to a declared cap, stops on the FIRST short page (no wasted request), and on the cap files the one shared pageCapNote sentence and answers null. readCarrierEvents (EVENT_PAGE_CAP 10) and readPullFiles (FILE_PAGE_CAP 3) keep their caps to the number; what they gain is that the third read can no longer hold a different default.

COMMENT_PAGE_CAP is 10 pages = 1,000 comments. Sized on this board, read 2026-09-17 off the open-issue list rows (550 rows listed, cross-checked against open_issues_count = 550):

So ten pages clears the whole board today with a page to spare, and it is the same ten EVENT_PAGE_CAP uses — a reader comparing two caps in one file should have to remember one number.

The input record

The diagnosis key stays comments, so every sentence already keyed to it still finds its diagnosis. Two declared fields are added to INPUT_RECORD_PAIR_FIELDS, one per thread this file reads:

pair.1.card-comments: 101 row(s)
pair.1.card-comment-pages: 2 of 10 page(s) requested — the ladder stopped on a SHORT page, so the thread is COMPLETE
pair.1.pr-comment-pages: 1 of 10 page(s) requested — the ladder stopped on a SHORT page, so the thread is COMPLETE

and, when the cap is what stopped the read:

pair.1.card-comment-pages: CAPPED — 10 of 10 page(s) of 100 comments each were requested and EVERY ONE came back full, so the tail is past the cap and the thread is UNREAD (UNJUDGED) — ⛔ never a truncated pool, ⛔ never a clean reading

A thread of exactly 100 rows and a thread whose tail was dropped are the same 100 row(s) in every other line the block prints; they differ here, because the complete one stopped on a short page and the truncated one did not stop at all. The request ledger PR #18681 added shows the same ladder from the other side — request #3 is now …/comments?per_page=100&page=1 and #4 is &page=2.

The pins

A new --self-test battery, #18683: the card-comment read pages to a cap — past 100 is UNJUDGED, ⛔ never a truncated pool, 27 cases, declared in SELF_TEST_BATTERIES with the roster floor raised 29 → 30. It drives the ladder with an offline page server that reproduces GitHub's own semantics and counts the requests; ⛔ no network. What it holds: the 101st claim ENTERS the pool and GOVERNS, and its line is what the limb reads; the same thread cut at 100 reads absent (the CONTROL — the reading the un-paged read produced); the newer claim past the boundary supersedes the older one inside it, and cut at 100 the superseded carrier's yes is what the limb reads; a capped read is null, which is neither missing nor absent nor a carrier but unreadable; the ladder stops on the first short page (2 requests for 101 rows, 1 for a short thread, 2 for exactly 100 — a full page is indistinguishable from a finished one); a page that came back unread ends the ladder and the record says the cap was NOT what stopped it; the input record declares and prints both ladder fields; the sibling caps are untouched; and all three reads render ONE cap sentence.

The census, and the triage's upgrade probe

Report-only, no state write. Over the 550 open rows (521 issues, 29 PRs) read on 2026-09-17:

The upgrade probe's result: the condition is NOT met today. The clause-② population is what a --pair/sweep derivation actually pairs, not what carries a queue label: the sweep derived 28 pairs from 29 open PRs, and the longest card thread among them is 14 rows (#17534). The two open PRs that mention a 100+-comment card in prose — #18786 (#6015, #7623) and #18765 (#6024) — deliver #18693 and #18652 respectively, both under 10 comments; driven live before and after, both answer exit 0 with an identical pair reading. So no recorded --pair verdict on this board today was taken on a truncated pool, and the triage's p1 condition (「找到任一进入条款②认领池、评论数 > 100 的卡并驱动一次」) has no live instance to drive. The exposure is one PR away rather than realised: #13799 is pm:queue at 117 and enters the population the moment a PR delivers it.

The cost is unchanged by the ladder, measured on the same board: 64 reads for 28 pairs, before and after, because every live thread fits one page and the ladder stops on a short page. The live --pair 18765 input records differ in exactly three lines — the two request paths gaining &page=1, and the two new ladder fields.

⚠️ One thing the two full sweeps do NOT compare: the sweep's finding COUNT moved 4 → 3 between them, and that is the board, not this diff. needs:contract-review was hung on PR #18792 at 2026-09-17T21:04:46Z, between the two runs, closing the C1 split on #18792 / #17541 on its own. The controlled A/B is the --pair 18765 diff above.

The ablation

Two legs, each from the COMMITTED fix, each proving the mutation reached disk by blob hash and occurrence count before reading any result, each restored under a trap with git checkout HEAD -- and verified by hash and an empty git diff HEAD. HEAD blob ccd5ad7c9a00fe703d644be261a24f1ed847915a.

leg mutation blob after self-test
A — the ENTRY side COMMENT_PAGE_CAP 10 → 1 1a0a952d07748101937420006b9475042d93a5cb exit 1, 11 of 865 red — the 101st-claim pins, the superseding pins, the request-count pins, the input-record pin
B — the UNJUDGED side the cap branch returns the pages that DID arrive (the pre-fix fail-OPEN default) c176dfe7e54e7de6bc45737487841a346f509b79 exit 1, 3 of 865 red — a capped read is no longer null, no longer unreadable, and files no cap sentence

Every red in both legs belongs to the new battery; nothing pre-existing went red in either. A third, unplanned reading came for free: leg B's first attempt was a perl -0pi substitution whose anchor contained a /, so the edit silently did nothing — the on-disk proof refused it with ABLATION VOID: the edit did not reach disk instead of reporting a green as a measurement.

Self-test

✓ check-clause2-carriers self-test: 865 cases pass

838 before, 865 after — the 27 the new battery registers, which is what its floor pins.

Derived gates

node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack, no hand-fed path list, re-derived after each origin/main merge (identical list both times). All 34 run at head 993cb89e18, each exit code captured by redirect-then-$?:

node scripts/check-adr-0087-registration.mjs --base origin/main :: exit 0
node scripts/check-adr-0087-registration.mjs --self-test :: exit 0
node scripts/check-changeset-no-major.mjs --base origin/main :: exit 0
node scripts/check-changeset-no-major.mjs --self-test :: exit 0
node scripts/check-ci-filter-parity.mjs :: exit 0
node scripts/check-closing-keyword-parity.mjs :: exit 0
node scripts/check-closing-keyword-parity.mjs --self-test :: exit 0
node scripts/check-comment-mask-corpus.mjs :: exit 0
node scripts/check-declaration-mirrors.mjs :: exit 0
node scripts/check-declaration-mirrors.mjs --self-test :: exit 0
node scripts/check-scripts-symbol-anchors.mjs :: exit 0
node scripts/check-scripts-symbol-anchors.mjs --self-test :: exit 0
node scripts/check-self-test-wired.mjs :: exit 0
node scripts/check-self-test-wired.mjs --self-test :: exit 0
node scripts/check-self-test-workflow-commands.mjs :: exit 0
node scripts/check-self-test-workflow-commands.mjs --self-test :: exit 0
node scripts/check-whole-set-label-write.mjs :: exit 0
node scripts/check-whole-set-label-write.mjs --self-test :: exit 0
node scripts/pm/bare-root-worklist.mjs --self-test :: exit 0
pnpm check:agent-test-spelling :: exit 0
pnpm check:bash32-floor :: exit 0
pnpm check:changeset-gate-self-tests :: exit 0
pnpm check:cli-command-ids :: exit 0
pnpm check:cross-package-test-inputs :: exit 0
pnpm check:driver-memory-census :: exit 0
pnpm check:entry-guard :: exit 0
pnpm check:nul-bytes :: exit 0
pnpm check:parse-guard :: exit 0
pnpm check:pm-clause2-carriers :: exit 0
pnpm check:pm-dispatch-gates :: exit 0
pnpm check:pnpm-filter-targets :: exit 0
pnpm check:ratchet-remedy-authority :: exit 0
pnpm check:refd-timer-probe :: exit 0
pnpm check:watch-hint-literal :: exit 0
pnpm lint :: exit 0

--ran reconciles 34 derived / 34 run / 0 UNRUN. pnpm check:pm-dispatch-gates was run detached to a file — 1,788 cases, 748.6s on this box — and waited on in the foreground rather than under a timeout, so it is a measurement and not a SIGTERM.

Out of scope, deliberately

#18764 (a decorated **Claim:** never enters the pool — the ENTRY side) was read and NOT folded in: this card is WHICH rows reach the reader, not what the reader does with them, and the two repairs touch different lines. claimRetractions (PR #18770, the EXIT side) was read for the words it uses and not touched. The header's request-budget paragraph is amended in the same commit, because it stated "2 reads per card" as a fact and the thread is now a ladder — a cost statement that stopped being true is the shape this file exists against.

skip-changeset: scripts/pm/** ships in no package's files[], so nothing published moves.


Generated by Claude Code

…ast 100 is UNJUDGED rather than a truncated claim pool

`readCardComments` issued ONE `per_page=100` request with no `page=` ladder and
no short-read check, while the two sibling list reads in the same file paged to
a cap and answered `null` on it. One file, two opposite defaults on "I did not
read everything" — and the fail-OPEN one was the read that arbitrates ownership:
the governing-claim pool, and the `Clause-②` declaration read from it.

All three list reads now go through one `pagedListRead` ladder with one cap
sentence. `COMMENT_PAGE_CAP` is 10 pages (1,000 comments), sized on the board.
The input record states, per thread, the pages issued and whether the cap
stopped the read.

Claude-Session: https://claude.ai/code/session_01Gqi43smmqjJ5sUrhfoPeKu
Co-authored-by: Claude <noreply@anthropic.com>
@os-justin os-justin added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Sep 17, 2026 — with Claude
@os-justin
os-justin marked this pull request as ready for review September 17, 2026 22:04
@os-justin
os-justin added this pull request to the merge queue Sep 17, 2026
Merged via the queue into main with commit e08fae8 Sep 17, 2026
37 checks passed
@os-justin
os-justin deleted the claude/issue-18683-card-comments-page-ladder branch September 17, 2026 22:28
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Sep 28, 2026
…h the sibling's one reading (objectstack-ai#18824)

Fixes objectstack-ai#18764

Clause-②: no

## The defect

`scripts/pm/check-clause2-carriers.mjs` built the governing-claim POOL,
and indexed retractions, by testing the imported `CLAIM_COMMENT_MARKER`
against the RAW comment body. That constant anchors the bare word at
line start and tolerates leading whitespace and one blockquote marker —
nothing else — so a claim a seat wrote as a bolded or backticked
`Claim:` was not SUPERSEDED here, it was never a candidate: not listed,
not rejected, not named anywhere in the record. Meanwhile
`claimGovernance`, imported from the same sibling, has read both
ownership markers through `markerMatches` since objectstack-ai#18680, so ONE FUNCTION
held both answers at once — governance saw the bolded claim and the pool
beside it did not.

## The before-reading, re-derived here and not taken from the card

Taken through this file's own offline reader (`--pair-json`), on
`origin/main` `e7eb4e9184` — the before leg was run in the shared
checkout, whose copy of both `scripts/pm/check-clause2-carriers.mjs`
(blob `ccd5ad7c9a`) and `scripts/pm/check-half-states.mjs` (blob
`153d10a015`) is byte-identical at `e7eb4e9184` and at `a7bafc29af`, so
the reading is a reading of this branch's base.

| fixture | BEFORE (`e7eb4e9184`) | AFTER (this branch) |
|---|---|---|
| a bolded `Claim:` carrying its own `Branch:` and `Clause-②: no` | exit
**4**, row **C2 — MISPLACED**; `claim.selected: none — no comment on
this thread carries a line beginning \`Claim:\``; `claim.clause2-line:
(no carrier, so no line was read from one)` | exit **0**;
`claim.selected: 1 comment(s) in the pool`; `claim.clause2-line:
DECLARED \`no\`` |
| the backticked spelling, same lines | exit **4**, row **C2 —
MISPLACED**, identical record | exit **0**, `DECLARED \`no\`` |
| a BARE older claim declaring `yes` beneath a DECORATED newer one
declaring `no` | exit **4**, row **C3**; the OLD claim governs;
`claim.clause2-line: DECLARED \`yes\`` | exit **0**; the decorated newer
claim governs; the older is listed `rejected: 1 … a SUPERSEDED claim`;
`claim.clause2-line: DECLARED \`no\`` |

The third row is the expensive direction and it is why this is not
merely a missing reading: the value the declaration limb hands every
downstream reader FLIPS, and the wrong value produced an adverse C3 row
about a contract-review gate that was never owed.

### One premise of the dispatch is falsified, and it is stated rather
than smoothed over

The escalation's two live bodies (objectui#9660 comment `5721120402`,
objectui#9717 comment `5720184809`, read 2026-09-17T22:02Z) carry **no
`Branch:` directive line at all**. Driven through the pool verbatim,
both read `claim-branch-unparsed` and exit **2** on BOTH sides of this
change — `claim.selected: NONE — the newest claim comment (5721120402 at
2026-09-17T20:57:09Z) parses ZERO branches`. That state is already
correct on `main`, because it is resolved by the sibling's governance
leg, which already reads decorated claims. So the fixtures in the table
above ADD the `Branch:` / `Clause-②` lines: the defect this PR repairs
is the one that only becomes visible once a claim is otherwise complete.
Neither shape ever read `absent`.

## The repair: one path, and it is the sibling's

Both former raw tests now read through
`markerMatches(CLAIM_COMMENT_MARKER, body)`, imported from
`check-half-states.mjs` — the pool (`claimCarrierSelection`) and the
retraction indexer (`claimRetractions`). The bare marker is tried first
inside `markerMatches`, so the change is provably additive: no body that
matched yesterday stops matching.

- No `\*\*` was added to `CLAIM_COMMENT_MARKER`. Decoration is an open
set, so admitting one spelling buys exactly that spelling; the constant
is the protocol's spelling and every reader imports it rather than
restating it.
- No second undecorator was written in this file. That would give this
file a private definition of "decorated" for the sibling to drift from,
which is the failure the card names.
- The near-miss vocabulary (`OWNERSHIP_MARKER_NEAR_MISS_FORMS`) is the
sibling's and is not re-declared here.
- `scripts/pm/check-half-states.mjs` is untouched.

## The retraction stripper, measured against that path — it STAYS, and
that is a reading

`undecorateRetractionLine` (this file) and the sibling's
`undecorateProseLine`-through-`markerMatches` were run over one fixture
set, with the retraction stripper extracted from the shipped source text
rather than retyped. **5 of 12 fixtures read differently:**

| fixture | `undecorateRetractionLine` | `markerMatches` (shared) |
same? |
|---|---|---|---|
| bare `Claim:` | true | true | yes |
| bold | true | true | yes |
| backticks | true | true | yes |
| **underscore emphasis** `__Claim:__` | true | **false** | **NO** |
| leading blockquote | true | true | yes |
| blockquote + bold | true | true | yes |
| **list item (hyphen)** `- Claim:` | true | **false** | **NO** |
| **list item (asterisk)** `* Claim:` | true | **false** | **NO** |
| **sigil prefix** `🚨 Claim:` | true | **false** | **NO** |
| a line that is NOT a marker but starts with the word | false | false |
yes |
| inflected word `Claiming:` | false | false | yes |
| **heading** `## Claim:` | true | **false** | **NO** |

Per the dispatch, the retraction stripper is **kept as is** and is ⛔ NOT
unified by hand. The divergence is load-bearing in both directions,
which is why: the objectstack-ai#18373 retraction this file's own battery replays
opens with a leading sigil, and narrowing the retraction path to the
shared reading would stop reading the specimen objectstack-ai#18719 landed for;
widening the shared path the other way would make a list item a claim,
which H20 pins as not one. Two strippers, two jobs — now stated in the
file, and pinned, rather than discovered again. The unification question
is for the seat to route; it is not folded in here.

## The pins

A new self-test battery, `objectstack-ai#18764: a DECORATED claim ENTERS the pool —
ONE reading, and it is the sibling's`, 24 cases, with
`SELF_TEST_BATTERY_FLOOR` raised by exactly the one battery it adds (30
→ 31). It pins: a bold and a backticked claim ENTER the pool and GOVERN,
with their `Branch:` and `Clause-②` lines read from them; a decorated
NEWER claim supersedes a BARE older one and the value the limb reads
moves with it; the older record is listed SUPERSEDED rather than
dropped; the retraction index sees a decorated claim as retractable by
its own author; the input record names the decorated row it selected, by
id and by date.

The controls are the other half: the constant itself still refuses those
same bodies; a list item, a heading and an underscore-emphasis line are
still not claims; the `Clause-②-correction:` comment does not enter the
pool through the new door either; a bare claim reads exactly as before;
and the two undecoration paths are pinned as the two jobs they are. The
bare-constant pins that assert on the CONSTANT
(`CLAIM_COMMENT_MARKER.test(...)` in the objectstack-ai#17366 and objectstack-ai#17149 batteries)
are byte-untouched. No network in the self-test.

## Census — report-only, over THIS gate's own population

The population is the one this gate derives: the open PRs of one board
and the cards they deliver, by this file's own `derivePairs`. ⛔ This is
NOT the half-states population the triage scanned (46 cards / 253
comments over two boards).

- **`objectstack-ai/objectstack`, read 2026-09-17T22:43:06Z** — 26 open
PRs, 24 derived pairs, 24 distinct cards, 147 comments read, 0 unread.
[LIT CTRL] bare-marker claim comments in today's pool: **29**. Decorated
claims the shared reading sees and today's pool does not: **0**.
- **`objectstack-ai/objectui`, read 2026-09-17T22:44:02Z** (the board a
seat reaches with `PM_SWEEP_REPO`, named because the escalation's
instances live there) — 14 open PRs, 12 derived pairs, 12 distinct
cards, 69 comments read. [LIT CTRL]: **13**. Decorated claims invisible
to today's pool: **2**.
- card objectui#9629, comment `5721993574` (`os-sales`,
2026-09-17T22:19:38Z), bolded — the card's ONLY claim. Before: pool
empty, declaration `misplaced no` (an adverse C2). After: pool = that
comment, declaration `declared no`.
- card objectui#9317, comment `5653084747` (`claude[bot]`,
2026-09-13T11:51:39Z), backticked — the card's ONLY claim. Before: pool
empty, `claim-branch-unparsed`. After: in the pool, still
`claim-branch-unparsed` — the gate now names the carrier by id instead
of reading the thread as unclaimed.

⚠️ Every instance above carries its reading time because it may have
left this state since. ⛔ No state was written by this census.

## Ablation, from the committed fix

Two legs, each mutating on disk from the committed state, each proven to
have reached disk by blob hash, each restored under a `trap` and
verified by blob hash plus an empty `git diff HEAD`.

| leg | mutation | blob before → after | self-test |
|---|---|---|---|
| the POOL predicate back to the raw `.test` | `claimCarrierSelection` |
`f7ee45d4ef` → `ae548569fd` | exit **1** — **8 of 889** cases red, all 8
from the new battery, nothing pre-existing red |
| the RETRACTION indexer predicate back to the raw `.test` |
`claimRetractions` | `f7ee45d4ef` → `708893dc5c` | exit **1** — **2 of
889** cases red, both from the new battery, nothing pre-existing red |

Both restorations: `RESTORED: blob
f7ee45d, git diff HEAD empty`.

⚠️ Worth recording: with the fix in place and the new battery removed
from the picture, the pre-existing 865 cases pass **unchanged** on both
sides of the repair. The existing suite could not see this defect, which
is exactly why the battery is the deliverable and not an extra.

## Self-test

`node scripts/pm/check-clause2-carriers.mjs --self-test` — 865 cases
before, **889 cases pass** after (865 + 24), exit 0; also green as `pnpm
check:pm-clause2-carriers` on the final head.

## Derived gates, with exit codes

Derived from this worktree with `node scripts/pm/dispatch-gates.mjs
--commands --repo objectstack-ai/objectstack` (no hand-fed path list) at
`984a93a173`: change set 1 path,
`scripts/pm/check-clause2-carriers.mjs`, three-dot against merge base
`a7bafc29a`. **34 derived commands, 34 run, all exit 0**, reconciled
with `--ran` recording each exit code: `34 derived famil(ies) accounted
for — 34 run, 0 NOT-MEASURED (a DERIVED zero)`.

`node scripts/check-adr-0087-registration.mjs --base origin/main` ·
`--self-test` · `node scripts/check-changeset-no-major.mjs --base
origin/main` · `--self-test` · `node scripts/check-ci-filter-parity.mjs`
· `node scripts/check-closing-keyword-parity.mjs` · `--self-test` ·
`node scripts/check-comment-mask-corpus.mjs` · `node
scripts/check-declaration-mirrors.mjs` · `--self-test` · `node
scripts/check-scripts-symbol-anchors.mjs` · `--self-test` · `node
scripts/check-self-test-wired.mjs` · `--self-test` · `node
scripts/check-self-test-workflow-commands.mjs` · `--self-test` · `node
scripts/check-whole-set-label-write.mjs` · `--self-test` · `node
scripts/pm/bare-root-worklist.mjs --self-test` · `pnpm
check:agent-test-spelling` · `pnpm check:bash32-floor` · `pnpm
check:changeset-gate-self-tests` · `pnpm check:cli-command-ids` · `pnpm
check:cross-package-test-inputs` · `pnpm check:driver-memory-census` ·
`pnpm check:entry-guard` · `pnpm check:nul-bytes` · `pnpm
check:parse-guard` · `pnpm check:pm-clause2-carriers` · `pnpm
check:pm-dispatch-gates` · `pnpm check:pnpm-filter-targets` · `pnpm
check:ratchet-remedy-authority` · `pnpm check:refd-timer-probe` · `pnpm
check:watch-hint-literal` — each exit 0.

`pnpm check:pm-dispatch-gates` was run DETACHED with output to a file
and read from that file: 1809 cases pass, 747.5s on this box, exit 0. ⛔
It was never run under a foreground timeout, so no reading here is a
SIGTERMed run.

Repo-wide `pnpm lint` (`eslint . --no-inline-config`): exit 0, run on
the final head `984a93a173`.

## Scope

`skip-changeset`: the only changed file is
`scripts/pm/check-clause2-carriers.mjs`, which no package ships —
nothing published moves.

objectstack-ai#18807 (`listOpenPulls`'s bare page cap, behind this card on the same
file) was read and is not addressed here: its lines are untouched by
this diff. PR objectstack-ai#18799's `pagedListRead` ladder and PR objectstack-ai#18770's
`claimRetractions` were read for the words the record and the selector
use, and neither was touched.

## Acceptance notes

- The two undecoration paths in this file diverge on 5 of 12 fixtures
(table above). Whether the protocol should converge them — and in which
direction — is a real question and is deliberately left to the seat
rather than answered by a grep. Noted, not filed by the dev.
- On a thread whose newest claim is decorated and parses zero branches,
`claim.clause2-line` now reads from that comment instead of printing
`(no carrier, so no line was read from one)`, because the pool is no
longer empty on it. This makes the decorated case render exactly as the
bare case already did; `claim.selected` still reports `NONE … parses
ZERO branches` and the pair is still UNJUDGED. Stated as a measured
rendering delta, not a verdict change.

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

size/m skip-changeset PR has no user-facing published change; bypasses the changeset gate

Projects

None yet

2 participants