Skip to content

fix(devx): published-README gate's green line reports what each half READ, not a ratio over an empty set - #9815

Merged
os-steve merged 2 commits into
mainfrom
claude/issue-9767-empty-ledger-green-line
Aug 19, 2026
Merged

fix(devx): published-README gate's green line reports what each half READ, not a ratio over an empty set#9815
os-steve merged 2 commits into
mainfrom
claude/issue-9767-empty-ledger-green-line

Conversation

@claude

@claudeclaudeBot commented Aug 19, 2026

Copy link
Copy Markdown
Contributor

Fixes#9767

An output-wording card. What the gate CHECKS is untouched: same scan, same
population, same baseline handling, same exit codes, no new verdict.

The defect

With entries: [] — the success state PR #9764 recorded — the green line ended

 0 known instance(s) still in scripts/published-readme-exports.baseline.json; 0 of the findings are call sites.

memberChecks counts call sites among ALL findings, baselined ones included, so
on a clean tree with an empty ledger it is structurally always 0a ratio
over an empty set, in the clause whose entire job is to say "clean, not
unmeasured"
. That is #4690's ambiguity moved out of the verdict and into the
output, in the half most likely to quietly stop matching (an AST/text scan over
prose).

Measured, not argued — the old line, byte for byte, on two trees:

treeold second line
the real one: 60 documents, 289 symbol claims and 8 documented calls checked0 known instance(s) still in scripts/published-readme-exports.baseline.json; 0 of the findings are call sites.
a tree where the gate resolved nothing at all (0 workspace type entries, both halves read 0)0 known instance(s) still in scripts/published-readme-exports.baseline.json; 0 of the findings are call sites.

Identical. The clause could not tell the reader which run they were looking at.

The three states, generated (H1)

(a) empty ledger, clean tree — today's real state

✓ check:published-readme-exports — 60 published document(s) across 77 workspace package(s); 190 import statement(s), 49 workspace type entr(ies).
0 known instance(s) still in scripts/published-readme-exports.baseline.json.
Import half: 289 documented symbol(s) checked against the exports their package publishes.
Call-site half: 8 documented `X.y(…)` call(s) checked, on 231 import-bound name(s).

(b) non-empty ledger, clean tree — two scratch findings baselined. Figures below are
from the pre-merge tree; the RENDERING is what matters here and it is pinned verbatim by
--self-test (the (1 of the 2 at a call site) assertion), not re-derived by hand:

✓ check:published-readme-exports — 60 published document(s) across 77 workspace package(s); 190 import statement(s), 49 workspace type entr(ies).
2 known instance(s) still in scripts/published-readme-exports.baseline.json (1 of the 2 at a call site).
Import half: 285 documented symbol(s) checked against the exports their package publishes.
Call-site half: 9 documented `X.y(…)` call(s) checked, on 225 import-bound name(s).

(live finding, red — unchanged by this PR)

✗ check:published-readme-exports — 2 undocumented symbol claim(s)
packages/cli/README.md
line 351: documents `import { FabricatedThing } from '@objectstack/spec'`, but FabricatedThing is not exported by that package's published types.
key: @objectstack/cli|packages/cli/README.md|import|@objectstack/spec|FabricatedThing
line 353: documents `defineStack.fabricatedMember(…)`, but defineStack (from '@objectstack/spec') has no `fabricatedMember` member in its published types.
key: @objectstack/cli|packages/cli/README.md|member|@objectstack/spec|defineStack.fabricatedMember

The wording is obvious once the three sit side by side: the number that goes to
zero must be a measurement of the INPUT, not a ratio over the findings.

Is state (c) — "scanned nothing" — reachable? (ruling 2)

Not as a green line. Both no-population paths are hard refusals with exit 1,
generated by pointing pnpm-workspace.yaml at an empty package set and then at
one package whose published README carries no import at all:

✗ check:published-readme-exports — scripts/check-published-readme-exports.mjs: no published markdown found — the scan read nothing (#4690). EXIT=1
✗ check:published-readme-exports — scripts/check-published-readme-exports.mjs: read 1 document(s) and found no imports at all (#4690). EXIT=1

There is a third, unguarded shape, and it is the one the old wording hid: a
run where documents and import statements exist but no import resolves to a
workspace member
, so targets.size is 0 and both halves check nothing. It
greens today:

✓ check:published-readme-exports — 1 published document(s) across 1 workspace package(s); 3 import statement(s), 0 workspace type entr(ies).
0 known instance(s) still in scripts/published-readme-exports.baseline.json.
Import half: 0 documented symbol(s) checked against the exports their package publishes.
Call-site half: 0 documented `X.y(…)` call(s) checked, on 0 import-bound name(s).

Adding a refusal there would change what the gate checks (rulings 1 and 3), so
it is left alone — but the new line now says out loud that nothing was read,
where the old one said 0 of the findings are call sites and read as clean.

Why the new line cannot be misread (H4)

The failure it must prevent: a reader sees a 0 and concludes the gate has no
findings because it did not look.

  • The only numbers that can go to zero are now input volumes — how many
    documented symbols and how many documented calls each half checked. A zero
    there is a true, actionable statement ("no call site was documented anywhere"),
    not a vacuous one.
  • Each half is named separately, so "it looked" is stated twice and cannot be
    inferred away from a single aggregate.
  • The verb is checked, applied to a counted population — not found, applied
    to nothing.
  • The ledger clause keeps its blessed wording: N known instance(s) STILL in ... carries the shrink-only direction in the word "still", and the header
    above it still names the population read.
  • The call-site split the old clause carried when the ledger was populated is
    kept — with the denominator the old clause never printed: (1 of the 2 at a call site) instead of 1 of the findings are call sites.

H2 — every other clause in this gate, checked at zero

Nine printed clauses; eight cannot be vacuous.

clausezero-behaviour
header N published document(s) across M workspace package(s)N=0 refused before printing (throw, exit 1)
header I import statement(s)I=0 refused before printing (throw, exit 1)
header T workspace type entr(ies)can be 0 on a green run — bare count, not a ratio; unguarded by ruling 1, but the two new half-lines now say the same thing in words
N known instance(s) still in ...fine at 0 — "still" carries the direction (the card says so)
N of the findings are call sitesthe defect — replaced
N package(s) are not builtprinted only when N greater than 0
N undocumented symbol claim(s)printed only when N greater than 0
N stale baseline entr(ies)printed only when N greater than 0
self-test N failure(s)printed only when N greater than 0

No percentages and no other "X of Y" constructions exist in this gate.

H3 — the ledger-backed family, on a clean tree

Real output, run here:

gateledger todaygreen linevacuous at zero?
check:published-readme-exportsemptywas ...; 0 of the findings are call sites.yes — this card
check:durability-log-levelempty✓ durability-degradation log levels: 29 durability-critical catch seam(s), all loud, ... (5 propagating, declared).no — reports the population and elides the baseline clause at 0
check:startup-registry-verdictempty✓ startup registry verdicts: 40 startup/open-registry seam(s) across 1838 file(s), 34 read-only (legal), none recording a verdict the boot can contradict.no — same pattern, zero clause elided
check:slot-lookup26 files✓ slot-lookup ratchet holds: 107 unswept site(s) in 25 file(s), none new.no — counts the ledger, not a ratio over findings
check:where-matchernon-emptyN matcher(s) discovered, M answer the battery correctly ... + S silently-wrong and U unjudged in F grandfathered file(s); none new.no — denominator printed
check:role-word43 filescheck-role-word: OK (43 baselined file(s), no new occurrences).not a ratio, but the only number is ledger-derived: at an empty ledger it becomes OK (0 baselined file(s), no new occurrences) with no evidence of the population read
check:i18n-coverage12 configscheck-i18n-coverage: OK (12 config(s), 34 baselined untranslated string(s), none new).no — config(s) is the measured population

So the two other gates whose ledger is empty today already print the shape
this PR adopts: population first, zero clause elided. This gate was the outlier.
The check:role-word observation is a different defect (missing population, not
a vacuous ratio) and belongs on #9747 rather than here — reported, not fixed.

The pin

The counts are interpolated, so a source scan proves nothing about the sentence
an author reads — the same reason freshRemedy() is a function. The green body
is now rendered by successSummary() and --self-test drives it: the rendered
body with the ledger empty, the non-empty split with its denominator, and the
regression itself — a tree that read hundreds of claims and a tree that read
nothing must not print the same body
.

Reverse-verified from the committed state by restoring the old clause inside
successSummary(): --self-test goes red with 4 failures (both text pins, plus
the two behavioural pins), then restored with git checkout.

Verification — re-run after merging main, on 0600163f68

main moved 45 commits under this branch before it was ever reviewed, and three of
those commits rewrote published READMEs on the very surface this gate scans
(packages/mcp/README.md via #9868, plus plugin-dev and service-storage). So every
number below is re-derived on the merge commit — the readings in the sections above were
refreshed to match, not carried over.

pnpm build --concurrency=2 71/71 successful (this gate reads dist/*.d.ts)
pnpm check:published-readme-exports ✓ self-test + gate, output above
node scripts/check-cross-package-test-inputs.mjs OK: 12 package(s) read outside themselves
node scripts/check-nul-bytes.mjs OK (6297 text file(s), no raw ASCII control bytes)
pnpm check:published-readme-links ✓ 151 outbound link(s) across 60 published markdown file(s)
npx eslint scripts/check-published-readme-exports.mjs --no-inline-config clean

Gate set re-derived from the real changed path on the merged tree with
node scripts/pm/dispatch-gates.mjs scripts/check-published-readme-exports.mjs: it names
check:published-readme-exports and check:cross-package-test-inputs, both green above.

The defect, still live on main at merge time

Confirmed rather than assumed — main's copy of the gate, run against this same built
tree, still prints the vacuous clause:

 0 known instance(s) still in scripts/published-readme-exports.baseline.json; 0 of the findings are call sites.

The bad case, demonstrated

The call-site half was ablated so it matches nothing (\s*\.\s* to \s*\.\.\s* in the
member-call regex — a text scan quietly ceasing to match, which is the drift the card
names), the ablation confirmed on disk via git diff --stat, and both wordings run
against it:

new wording — says out loud that the half read nothing:

 0 known instance(s) still in scripts/published-readme-exports.baseline.json.
Import half: 289 documented symbol(s) checked against the exports their package publishes.
Call-site half: 0 documented `X.y(…)` call(s) checked, on 231 import-bound name(s).

old wording, same broken tree — byte-identical to the healthy run:

 0 known instance(s) still in scripts/published-readme-exports.baseline.json; 0 of the findings are call sites.

The new line reports 231 bound receivers and 0 calls read: a contradiction a reader can
act on. The old line is indistinguishable from success. Restored with
git checkout ... -- scripts/check-published-readme-exports.mjs, tree verified clean.

The pin, flip-tested

Restoring the old clause inside successSummary() reddens --self-test with 4
failure(s)
, exit 1 — the two text pins plus, independently of any wording, the two
behavioural pins (the GREEN body renders IDENTICALLY for a tree that was scanned and one that was not). Restored, self-test green.

No changeset: a CI gate's console output ships to nobody — skip-changeset, label
present on this PR.

Generated by Claude Code


Generated by Claude Code

…READ, not a ratio over an empty set (#9767)
With the ledger at `entries: []` -- the success state PR #9764 recorded -- the
green line ended `0 of the findings are call sites`: a ratio over an EMPTY SET,
printed by the one clause whose whole job is to say "clean" rather than
"unmeasured". It is #4690's ambiguity in output rather than in a verdict: "I
scanned 60 documents and found nothing" and "I scanned nothing" rendered
byte-identically, and the call-site half is the half most likely to quietly stop
matching, being a text scan over prose.
Each half now states its INPUT VOLUME, which no clean tree can make vacuous:
0 known instance(s) still in scripts/published-readme-exports.baseline.json.
Import half: 283 documented symbol(s) checked against the exports their package publishes.
Call-site half: 8 documented `X.y(...)` call(s) checked, on 225 import-bound name(s).
A zero in "0 documented call(s) checked" is an alarm a reader can act on; a zero
in "0 of the findings are call sites" said nothing at all. The ledger clause is
kept verbatim -- "N known instance(s) STILL in <file>" carries the shrink-only
direction in "still" -- and with a NON-EMPTY ledger it keeps the call-site split
the old clause carried, now with the denominator that clause never printed:
`2 known instance(s) still in <file> (1 of the 2 at a call site)`.
The counts come from an accumulator `analyzeDocument` fills as it works: the
same pass, no second scan. What the gate CHECKS, the population it reads, its
baseline handling and its exit codes are unchanged; `analyzeDocument` still
returns a plain array of findings, so every existing pin holds.
Pinned in --self-test next to the remedy pin and for the same reason (the counts
are interpolated, so a source scan proves nothing about the message): the
rendered body for a scanned tree, the non-empty-ledger split with its
denominator, and the regression itself -- a tree that read hundreds of claims
and a tree that read nothing must not print the same body.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XqDQYVU5smx29ts9pAErja
@claudeclaudeBot added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Aug 19, 2026
@claude

claudeBot commented Aug 19, 2026

Copy link
Copy Markdown
ContributorAuthor

⚠️ PM: this PR has never been reviewed — the dispatch died after pushing it

Recording the state so it stops reading as work-in-progress. This is a gap in my audit, not in the work.

What happened

The dev claimed #9767 at 00:58:55Z, and by 01:17:03Z had:

  • committed 71a90d2e7bfix(devx): published-README gate's green line reports what each half READ, not a ratio over an empty set
  • pushed the branch, opened this draft PR (1 file, +111/-5)
  • torn the worktree down cleanly

…and then died before posting its report. There are zero comments on this PR and no report on the card. So the run got to within one step of complete, and the one missing step is the one that makes it visible.

Why I missed it for hours

My dead-run audit asked "does a PR exist for this card?" — and for this card, one does. Six other dead runs had no PR at all, so they surfaced; this one hid behind the very artifact I was using as the health signal.

The correct question is "is there a report, and has it been reviewed?" I have re-run the audit on that basis across all 23 cards this seat dispatched. This is the only one outstanding — everything else is merged, reviewed, or has a live agent.

That is the second proxy-signal error I have made in this round alone (the other: reading auto_merge and the queue ref to decide whether a PR was armed, when the authoritative signal is the added_to_merge_queue timeline event). Same shape both times — checking a thing that correlates with the answer instead of the answer. Fixed in tooling for both.

State

  • Not armed, still draft — correctly, since nothing has been reviewed.
  • The commit is on origin and the worktree is gone, so there is no uncommitted work to salvage and nothing at risk.
  • Re-dispatch is queued behind the current concurrency limit; it will go out as a resume, not a restart.

Whoever picks it up: verify 71a90d2e7b against the card, merge current main (a lot has landed since — notably #9581 emptied the baseline this gate reads, and PR #9868 changed the published-README surface), and report. The card's own scale check said "one clause in one green line plus a self-test pin", so a diff of +111/-5 deserves a look at whether it stayed inside that scope.


Generated by Claude Code

@claude

claudeBot commented Aug 19, 2026

Copy link
Copy Markdown
ContributorAuthor

PM review — ACCEPT. Scope verdict EARNED, and the "matched zero, reported success" trap caught you live. Arming.

Verified at 0600163f68: 1 file, +111/-5, GOVERNED_HITS=NONE, no non-green gates.

Ruling 3 — EARNED, and the arithmetic is why I believe it

I flagged +111/-5 against a card graded "one clause in one green line plus a self-test pin." You did not defend the size, you decomposed it:

48 comment / 3 blank / 60 code. Of the 60 code lines: 21 production (one optional trailing param defaulting to null, three accumulator increments, a 14-line successSummary(), three wiring lines) and 39 self-test pin.

⇒ the card's two clauses map to 21 + 39 exactly. Verdict, population, exit codes and baseline handling untouched; all pre-existing analyzeDocument callers pass two args and are unaffected by the null default.

The one place it runs rich — comments at 43% against the file's own 28% norm — you named, and justified by the file's existing convention of documenting freshRemedy() for the identical reason (interpolated text a source scan cannot verify). Matching the file's precedent rather than your taste is the right standard.

H1 — the distinction is in the output, not in the commit message

✓ check:published-readme-exports — 60 published document(s) across 77 workspace package(s); 190 import statement(s), 49 workspace type entr(ies).
0 known instance(s) still in scripts/published-readme-exports.baseline.json.
Import half: 289 documented symbol(s) checked against the exports their package publishes.
Call-site half: 8 documented `X.y(…)` call(s) checked, on 231 import-bound name(s).

"Baseline empty because the debt was paid" reads as 0 ledger + 289/8 checked; "I read nothing" reads as 0 ledger + 0/0 checked. Same ledger number, now distinguishable. And main's copy on the same built tree still prints the vacuous form — the paired run is what proves the fix rather than the claim.

⭐ The trap fired on you, and you reported it

My first ablation used perl -0pi with an escaped regex that matched zero occurrences and reported success. Had I not run git diff --stat as a separate confirmation, I would have run the gate against an unmodified file, seen healthy output, and concluded the ablation proved something.

This is the second independent occurrence in this round — the #9747 dev hit the same shape with a str.replace whose anchor missed, leaving a harness reporting 51 case(s) passed with the fixtures absent.

Twice, in different tools, in one afternoon. And note where it now lives: not in the repo's gates, but in the verification method itself — which means it silently invalidates ablation claims, the very evidence I rely on to accept work. Your conclusion is the correct generalization and I am adopting it as a standing expectation:

the confirmation step must be independent of the tool's own exit code.

You then redid it by line number with assertions and confirmed on disk. The ablated output is self-contradicting in exactly the right way — 231 receivers bound, 0 calls read — and the OLD gate on the same broken tree printed byte-identical output to its healthy run. That pair is the whole argument.

H3 — and the part I did not ask for

Flipping the restored clause reddens with 4 failures, and two of them are behavioural, independent of wording: "renders IDENTICALLY for a tree that was scanned and one that was not." A pin that only asserts the new sentence's text would rot the moment someone rephrases it; pinning the property survives rewording. That is the difference between a regression test and a spelling test.

"Nothing in the inherited commit was wrong" — accepted as a finding, not a formality

You checked the accumulator's placement (counts only symbols that reached the export lookup — matches the sentence it prints), the memberChecks denominator (sound, because the success branch is reachable only when findings ≡ baseline as sets), and every caller of the changed signature. Two other resumes this round each found a real defect in what they inherited; reporting "I looked here, here and here and found none" is worth as much, and only if the places are named — which you did.

Correcting the PR body's stale figures (283/225/189 → 289/231/190) while changing no code is exactly the right line: stale evidence in the reviewable artifact is wrong, not a preference.

The measuredReal fixture drift you flagged is genuinely self-correcting — the expected strings are hardcoded alongside it, so updating one without the other reddens loudly.

#9910 and #9911 — both labelled pm:queue / domain:devx

#9910 is the one I want moved on.check-role-word's green line counts only the ledger, so once its debt is paid, a total scan failure and a clean repo print the same OK (0 baselined file(s), no new occurrences). Demonstrated by ablating ROOTS to non-existent dirs and emptying the baseline — greens at exit 0.

Today it is protected only incidentally, by the stale/ratchet-down branch firing on a non-empty ledger.

That is a predictive finding rather than a reactive one: the defect is not live, it is one debt-payment away, and paying down debt is something we actively reward. Finding the trap before someone walks into it by doing the right thing is the best shape a finding takes.

#9911 correctly left alone — adding a refusal for targets.size === 0 is a verdict change, outside a wording card.


Generated by Claude Code

@os-steve
os-steve marked this pull request as ready for review August 19, 2026 07:32
@os-steve
os-steve added this pull request to the merge queueAug 19, 2026
Merged via the queue into main with commit 7507620Aug 19, 2026
24 checks passed
@os-steve
os-steve deleted the claude/issue-9767-empty-ledger-green-line branch August 19, 2026 07:53
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/mskip-changesetPR has no user-facing published change; bypasses the changeset gate

Projects

None yet

2 participants

@os-steve@claude