Skip to content

[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

@claude

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 private unset AND a non-empty files array 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's files array 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

⚠️ Note for whoever reads #10368's ruling: it states the runtime README holds 47 sites on 18 receivers. The gate's own population says 30 on 17, and no counting convention I could find in the file reproduces 47/18 (import-bound-only, i.e. pre-#10365: 35/20; raw occurrences with no dedup: 44; whole-file rather than fenced: identical). The document has not been edited since #10226, well before the ruling was written. 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:

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

  1. huangyiirene commented on Aug 21, 2026

    @huangyiirene
    Collaborator

    Triage first-touch: promoted to queue, type Task. Scope = the cheap shape the card names: an opt-in --unread-report flag 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

  2. claude commented on Aug 21, 2026

    @claude
    ContributorAuthor

    Claim: /pm-dispatch domain:devx execution seat.
    Session: session_01DdCnBGcHeufjrq7drTD3wt · Branch: claude/issue-10815-unread-report · Worktree: objectstack-issue-10815
    Container & model: M, mode:subagent, model: opus
    File surface: the check:published-readme-exports gate script (+ its self-test).

    Scope from triage, and ⛔ no wider: an opt-in --unread-report flag 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.

    ⚠️ This card exists because of a transcription error I made: card #10368 quoted 47 sites / 18 receivers when the gate yields 30 / 17 — 47 is derivedReceivers, a whole-repo scalar printed one line above NOT 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

    Report back to me; I review against GitHub and land it.


    Generated by Claude Code

  3. os-zhuang commented on Aug 21, 2026

    @os-zhuang
    Contributor
    {
      "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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions