Repository navigation
[finding] check:published-readme-exports prints its NOT read blind spot only as whole-repo scalars — a per-document hand read must re-derive the population outside the gate #10815
Description
Found while executing #10368, which commissioned a hand read of ONE document's share of check:published-readme-exports' NOT read: population. Filed unassigned, no pm:queue.
The observation
The gate prints the blind spot as two whole-repo scalars, and nothing else:
NOT read: 120 documented `X.y(…)` call(s) on 72 receiver(s) with no type this gate can reach
(free variables, parameters, globals, non-workspace imports). Visibility, not a verdict — see #9870.
#9870 added those two numbers so the green line could not read as coverage of documents. They do that. What they cannot do is answer the question the ruling on #9870's open question 1 then asked of them: which document holds how many, and which sites are they? That is the input to every per-document hand read, including the one #10368 commissioned.
There is no flag, no --json, no per-document breakdown. countUnreadCalls(markdown, readableNames) is exported and returns { calls, receivers } — counts, not the site list — and readableNames is the thing a caller cannot get: analyzeDocument builds its readable map internally (import-bound names, plus #9870's extractLocalBindings derivations) and returns only findings.
What that cost, concretely
To adjudicate the sites in #10368 I had to re-implement analyzeDocument's readable derivation outside the gate — the import-binding walk, splitSpecifier/resolveTypesEntry, then extractLocalBindings filtered to sources that are import-bound — and then re-implement countUnreadCalls' body to return the pairs instead of their cardinality.
That is precisely the failure mode this file's own header refuses elsewhere:
"Published" is
privateunset AND a non-emptyfilesarray AND the file matched by one of its patterns […] That definition is load-bearing for more than one gate now, and two gates deriving it separately would disagree the first time a package'sfilesarray changed — silently, each still green. So it is computed HERE and imported, never re-derived.
A hand read whose population silently disagrees with the gate's produces a defect count with the wrong denominator, and nothing anywhere goes red. The only evidence available that my re-derivation was the gate's population is that summing it over all 60 published documents reproduced the printed pair exactly (120 / 72) — a whole-run checksum that catches a systematic drift and would not catch one document's worth.
The population that motivates it, measured
Per-document, derived as above and checksummed against the printed totals (repo at 112a8c6, before #10368's repairs):
30 call(s) / 17 receiver(s) packages/runtime/README.md
10 call(s) / 3 receiver(s) packages/metadata/README.md
9 call(s) / 2 receiver(s) packages/drivers/driver-mongodb/README.md
8 call(s) / 7 receiver(s) packages/client-react/README.md
7 call(s) / 4 receiver(s) packages/plugins/plugin-auth/README.md
… 27 more documents, all ≤ 5
47 is, however, exactly the derivedReceivers figure printed one line above on the same green output (47 name(s) built from one) — a whole-repo quantity. A per-document breakdown the gate itself emits is what makes that class of transcription slip impossible.
Scope
Observation. ⛔ Distinct from the three cards already living in this file, and none of them is this:
- finding: check:published-readme-exports greens when documents and imports exist but NOTHING resolves to a workspace member — the one no-population shape it does not refuse #9911 — refuse the third no-population state (
targets.size === 0). - [finding] check:published-readme-exports' namespace-import branch can never fire —
namespaceSymbolis hardcodednullat the only call site #10367 — the namespace-import branch could never fire. - Hand-audit packages/runtime/README.md's 47 unread call sites — the one surface where a per-document read is priced to pay #10368 — a hand read of one document, no gate change at all.
If it is taken up, the cheap shape is an opt-in reporting flag (--unread-report) that prints what the run already computed per document, changing no verdict and no exit code — the same "visibility, not a verdict" register NOT read: already occupies. ⛔ It must not become a widening of what the gate types: that is #9870's option D, explicitly declined pending a census.
Refs: #9870 / PR #10365 (the widening and the NOT read pair) · #10368 (the hand read that needed the breakdown) · #4690 (zero is a broken scan).
Generated by Claude Code
Activity
huangyiirene commentedon Aug 21, 2026
Triage first-touch: promoted to queue, type Task. Scope = the cheap shape the card names: an opt-in
--unread-reportflag printing the per-document(calls, receivers)pairs and site lists the run already computes — no verdict change, no exit-code change, same "visibility, not a verdict" register. ⛔ Not a widening of what the gate types (#9870 option D stays declined pending its census). Deliverable includes the whole-run checksum property (per-document rows must sum to the printed scalars) as a self-test, so the breakdown can never drift from the totals. If the 47/18-vs-30/17 transcription slip in #10368's ruling is confirmed by the new output, leave a one-line note on #10368 rather than editing its record. Size/model suggestion: S, sonnet.Generated by Claude Code
claude commentedon Aug 21, 2026
Claim:
/pm-dispatchdomain:devx execution seat.Session:
session_01DdCnBGcHeufjrq7drTD3wt· Branch:claude/issue-10815-unread-report· Worktree:objectstack-issue-10815Container & model:
M,mode:subagent,model: opusFile surface: the
check:published-readme-exportsgate script (+ its self-test).Scope from triage, and ⛔ no wider: an opt-in
--unread-reportflag printing the per-document(calls, receivers)pairs and site lists the run already computes. ⛔ No verdict change, ⛔ no exit-code change, same "visibility, not a verdict" register. ⛔ Not a widening of what the gate types — #9870's option D stays declined pending its census.⭐ The deliverable includes a checksum property as a self-test: the per-document rows must sum to the printed scalars, so the breakdown can never drift from the totals. That is the part that makes this more than a convenience flag.
47 sites / 18 receiverswhen the gate yields30 / 17—47isderivedReceivers, a whole-repo scalar printed one line aboveNOT read:. The card's own framing is that the gate invites that mistake by printing only whole-repo scalars where a per-document number is needed. Your flag is the fix for the invitation, not for my slip.Standing constraints
docs/adr/**,.claude/**,skills/**,AGENTS.md,CLAUDE.md— propose wording in the report.content/docs/releases/**. ⛔ Nevergit stash—refs/stashis shared by every worktree of the repo.cmd > log 2>&1; ec=$?.pnpm --filter <typo>exits 0 with "No projects matched the filters" — a whole verification round can be fictitious and read as green #10853, measured today:pnpm --filter <name>with a name that does not exist printsNo projects matched the filtersand EXITS 0 — a whole verification round can read as green while nothing ran. Verify your filter matched; a negative control (--filter @objectstack/definitely-not-a-package) is the cheap way./home/user/objectstackis STALE. Measure againstorigin/mainor your own worktree — never the shared tree. That mistake bit this seat twice today.dispatch-gatesis blind to bothscripts/**gates — runcheck:entry-guardandcheck:parse-guardby hand.POST /issues/N/labels), never a whole-set PUT, and read back after the size labeler runs.mainhasConsole Pin Gatered for a known unrelated cross-repo reason (p0: main is RED — #10695 retired six spec/ui theme schemas that the PINNED objectui still imports, so Console Pin Gate cannot build the SPA #10856) — non-blocking, ⛔ not yours, ⛔ not a signal about your change.Report back to me; I review against GitHub and land it.
Generated by Claude Code
feat(scripts): --unread-report decomposes `NOT read:` per document, c…
os-zhuang commentedon Aug 21, 2026
{ "issue": 10815, "status": "done", "branch": "claude/issue-10815-unread-report", "pr": "https://github.com/objectstack-ai/objectstack/pull/10874", "premise_still_valid": true, "summary": "Added an opt-in `--unread-report` flag to check-published-readme-exports that decomposes the `NOT read:` pair per document — one row per document (unread calls / unread receivers) plus the site list with line numbers, sorted heaviest-first as a hand-read work queue. The scope-critical structure: `unreadCallSites` returns the sites and `countUnreadCalls` is now a CARDINALITY VIEW over it rather than a second walk; one statement in `analyzeDocument` produces the row and both scalars from the same object; `newMeasured()` defines the accumulator once for the run and the self-test alike. The whole-run checksum re-adds the rows before printing and, on mismatch, WITHHOLDS the rows and prints a refusal instead — a breakdown that disagrees with its own total is worse than none, since it lends the gate's authority to a population the gate did not measure. The refusal changes no verdict and no exit code and says in its own text that it is a defect in the script, not in any README. For the transcription invitation the card is about: the report header names `name(s) built from one` and `call(s) checked` as whole-repo scalars with NO per-document share here, and every row spells both numbers `unread …` so a copied figure carries its scalar with it. One thing the report now makes explicit that the scalar never did: `NOT read:`'s receiver count is a PER-DOCUMENT SUM, so a receiver named in two documents counts twice — stated in the header rather than left for a reader to assume it is a repo-distinct count.", "tests": "All at final commit `9b786ca271` (clean tree; `git rev-parse --short HEAD` from the same session as the gate union). ⭐ CHECKSUM, pinned not trusted — the rows come from the production `analyzeDocument` path (`newMeasured()` + two fixture documents, one of them read-clean so the `1 of 2` denominator is measured), and four drift directions each must refuse AND withhold: a row grown, the call scalar moved, the receiver scalar moved, a whole row lost. Both legs driven by ABLATION ON DISK, each mutation proved by before/after `grep -c` on the exact text (never an editor's exit code): (1) `measured.unreadCalls += unread.sites.length;` -> `... + 1;` — before: target=1/injected=0, after: target=0/injected=1 — `--self-test` EXIT=1, `✗ ... 14 failure(s)`, incl. `analyzeDocument — EVERY analyzed document gets a row, and the rows re-add to the scalars expected {...\"scalars\":[3,2]} actual {...\"scalars\":[5,2]}` and the row pin reading `actual []` (rows correctly withheld). (2) checksum condition -> `if (false && (...))` — before: original=1/neutered=0, after: original=0/neutered=1 — EXIT=1, `✗ ... 9 failure(s)`, all four drift directions reporting `must REFUSE` / `printed a per-document row`. RESTORE LEG for both, proved on disk (`grep -c` original=1, mutation=0, `git status --short` empty) and re-run: `--self-test` EXIT=0. No build/dist is involved — this is a plain .mjs run — so the ablation preflight is the on-disk grep pair alone. INTEGRATION, on a purpose-built two-package fixture workspace with real `.d.ts` files: without the flag EXIT=0, with the flag EXIT=0 and the report's `Σ 4 unread call(s) / 3 unread receiver(s)` reconciles with the `NOT read: 4 ... on 3 ...` scalar printed six lines above it in the same output; with a fabricated-symbol package added, EXIT=1 both with and without the flag and the report still prints. ⭐ NO-VERDICT-CHANGE MEASURED, NOT ASSERTED: `origin/main`'s copy of the script vs mine (no flag) on the same trees — stdout+stderr byte-identical and exit codes equal on the green fixture (0/0), the red fixture (1/1) and the real unbuilt checkout (1/1). GATES, derived with `node scripts/pm/dispatch-gates.mjs` with no paths passed (it took the change set from the merge base itself: 1 path, `scripts/check-published-readme-exports.mjs`), plus the two `scripts/**` gates that derivation is blind to; every exit code captured as `cmd > log 2>&1; ec=$?`, never through a pipe, and each verdict quoted from the gate's own line: `check:cross-package-test-inputs` (0) `OK: 13 package(s) read outside themselves, all declared, and turbo.json hashes every declared glob.`; `check:published-readme-exports --self-test` (0) `✓ check:published-readme-exports --self-test — ...`; `node scripts/check-ci-filter-parity.mjs` (0) `OK: all 82 declared cross-package glob(s) (71 unique) are covered by core or crosspkg, ...`; `node scripts/check-cross-package-test-inputs.mjs` (0) same OK line; `check:entry-guard` (0) `✓ check:entry-guard: 129 scripts/ file(s) — every entry guard goes through invoked-as.mjs; 87 export bindings, 77 of them inert on import`; `check:parse-guard` (0); `check:nul-bytes` (0) `check-nul-bytes: OK (scanned 6292 text file(s) ... no raw ASCII control bytes)`. ⚠️ NOT run locally: the REAL half of `check:published-readme-exports`, which needs a built workspace — on this unbuilt checkout it hard-errors by design (`194 package(s) are not built`, #4690), IDENTICALLY for `origin/main`'s script and mine. It runs in CI's `typecheck-consumers` job, which builds first and carries no label exemption, so the new self-test cases do execute there. ⚠️ Declared narrowing: I did not build the workspace to obtain the real-tree per-document numbers — see open_questions. No `pnpm --filter` was used anywhere in this round, so the #10853 zero-match-exits-0 trap had no surface here.", "open_questions": [ { "question": "The real-tree per-document numbers are not measured by this PR, so #10368's `47 sites / 18 receivers` vs the card's `30 / 17` is neither confirmed nor denied. Triage asked for a one-line note on #10368 IF the new output confirms it. Getting that output needs a full workspace build (the gate hard-errors on any unbuilt reached target), which I judged not worth a shared-container full build for a visibility flag whose correctness is pinned offline.", "options": [ "A — leave it: land the flag, and let the first person who runs it on an already-built tree (or a CI job invoked with the flag) post the one-line note on #10368.", "B — I build the workspace under the verify lock in a follow-up round and post the confirmed row for packages/runtime/README.md on #10368.", "C — add `--unread-report` to the CI invocation so the breakdown is in every lint log." ], "recommendation": "A. The flag's correctness does not depend on the real-tree numbers — it is pinned offline in both directions, and the no-flag path is byte-identical to origin/main on three separate trees. C is out of scope and against the card's own framing (opt-in, and ~170 lines of visibility in every green CI log is how a gate's output gets scrolled past). B is a real option if you want the #10368 note closed out today, but it costs a full workspace build in a container shared with parallel agents, for a number the card already published from its own re-derivation." }, { "question": "Governed-path wording I did NOT write (⛔ `.claude/**` is human-merge-only), offered for your judgment: the pm-dispatch skill's notes could record that `--unread-report` now exists, since the mis-quote this card was filed for was made from this gate's green output during a dispatch round.", "options": [ "A — no doc change; the flag documents itself in the script header and `--help`-style usage comment.", "B — one line in the pm-dispatch skill: `quoting a per-document figure out of check:published-readme-exports? run it with --unread-report — the whole-repo scalars on the green line have no per-document share.`" ], "recommendation": "A, unless you want the belt-and-braces. The output itself now refuses the confusion at the point of reading, which is strictly earlier than a skill note is consulted; B is cheap insurance but it is your file to write." } ], "out_of_scope_findings": [] }Generated by Claude Code
Generated by Claude Code
feat(scripts): --unread-report decomposes `NOT read:` per document, c…