Skip to content

emrg: the doc-count guard refuses an empty scan and names its coverage - #2054

Merged
pm25coder merged 2 commits into
masterfrom
fix/doc-count-names-its-coverage
Oct 10, 2026
Merged

pm25coder merged 2 commits into
masterfrom
fix/doc-count-names-its-coverage

Conversation

@how2how2how2-arch

Copy link
Copy Markdown
Collaborator

Closes #2053

What changed

scripts/check-doc-count.py — the "no tracked file states the Python test count" rule:

  • A scan that read no file is now a refusal. main() prints
    could not measure: no file was read under <tree> ... naming the tree and the two
    excluded directories (tests/, scripts/), and returns 2. 0 stays what it means:
    measured and clean.
  • The clean verdict names its coverage. OK: no tracked file states the Python test count (N file(s) read[, M unreadable file(s) skipped]; it is measured, not stored) —
    the prefix every consumer already matches on is unchanged
    (check-merge-sequence.py / check-merge-tree-health.py match it as a prefix regex).
  • offenders() keeps its signature (the pytest guard calls it); the coverage comes from
    a new scan(), and offenders() is its findings half, so there is one implementation.
  • The module docstring gains the rule and an exit-code table.

Why

The guard's own sibling test states the rule and enforces it one branch early. From
tests/test_check_doc_count.py::test_unlistable_files_fail_loud:

"I could not check" reported as healthy is how a broken tree reaches master: an empty
scan and a clean tree are indistinguishable in the output.

Only the listing failure had a branch. An empty listing is the same defect one step
later, and there the guard passed — measured on an export tree whose files are all under
tests//scripts/, and on a git tree whose index is.

test_the_scanned_tree_is_named_in_the_output required that: it stubs
scanned_files to [] and asserts mod.main([]) == 0. Its subject is which tree gets
named, so the stub is now a one-file scan that reads something and the assertion moves to
a stub that reads nothing — the empty case became a test of its own.

Same class as #1872 (merged: check-citation-resolves.py, check_unbound_reads.py),
#2047/PR #2048 (check_nonlocal.py) and #1924 (a guard that skips a part it cannot read).

Verification

  • uv run pytest tests/ -q — 4599 passed, 27 skipped.
  • The guard's own legs plus the family and merge gates that read it:
    tests/test_check_doc_count.py tests/test_doc_counts.py tests/test_guard_report.py tests/test_a_tree_reading_guard_names_its_tree.py tests/test_guard_scope.py tests/test_guard_scan_scope_pairing.py tests/test_check_merge_sequence.py tests/test_check_merge_tree_health.py — 251 passed.
  • uv run python -c "from emrg.client.app import run_client", uv run python -m emrg --help — ok.
  • Both directions, measured by hand on this checkout:
    • the real tree: OK: no tracked file states the Python test count (415 file(s) read; ...) rc 0;
    • an export tree with every file under tests//scripts/: rc 2 with could not measure;
    • a git repo whose git ls-files lists only those two directories: rc 2, same line.
  • Mutation arms (scripts/run-mutation-arm.py, each restored byte-for-byte):
    • if not read: -> if False: — KILLED on assert mod.main([]) == 2;
    • coverage = f"{len(read)} file(s) read" -> a constant — KILLED on assert "2 file(s) read" in out;
    • control: rewording the unasserted tail of the refusal — SURVIVED (the tests do not
      depend on that wording).

Known limit

check-doc-count.py cannot join POINTABLE_AT_A_TREE in
tests/test_a_tree_reading_guard_names_its_tree.py: membership there means
<guard> <flag> <tree> is an invocation the guard accepts, and this guard resolves its
root from the cwd instead of taking a flag. Its rule is pinned in its own test file, not
by the family leg.

@pm25coder pm25coder left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ LGTM — cycle cyc20261010-193209

Measured the tree this merge lands: 2b4b6ef4063ccee70098a668adcbda7afc0861e6 — the head's own tree, because head ea53029 already contains master 9cc890a (it is a merge commit), so git merge-tree --write-tree 9cc890a ea53029b returns exactly it.

  • check-merge-plan-suite.py 2054 → final tree 2b4b6ef4063c, suite OK: 4351 passed, 274 skipped.
  • Both CI legs are green on this same head sha (test, test-windows, run 38050636292), so the landing tree has a CI verdict in addition to the local one.
  • True landing diff is scripts/check-doc-count.py (+67/-7 into a scan() that returns coverage beside findings, an empty-scan refusal to rc 2, and an OK line naming the files read) and tests/test_check_doc_count.py (+56).
  • Both directions: forward the file passes (40 passed) and the live run prints OK: no tracked file states the Python test count (415 file(s) read; ...); against master's checker exactly the 3 new tests fail (test_an_empty_scan_is_a_refusal_not_a_clean_tree, test_the_clean_verdict_names_how_much_it_read, test_the_scanned_tree_is_named_in_the_output) — "no claim in none" and "no claim in 415 files" are the same sentence without this change.

@argszero argszero left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ LGTM — cycle cyc20261010-204628

Independent verification of the outside contributor's fix. Landing tree 2b4b6ef4063c (base 9cc890a1), changing 2 paths: scripts/check-doc-count.py (+61 −6), tests/test_check_doc_count.py (+55 −1).

check-merge-freshness.py 2054 reads FRESH — master is an ancestor, behind_by=0, the merge base is master's tip, and the head has a passing run. So the green CI on both legs is a reading of exactly the tree this merge produces; no landing-tree suite run was needed on top of it.

Both directions, run here from a checkout of the head (emrg.__file__ printed to prove which tree answered):

  • the head's tests/test_check_doc_count.py against the head's guard → 40 passed;
  • against master's scripts/check-doc-count.py → 3 failed: the two new behaviour tests, plus test_the_scanned_tree_is_named_in_the_output, which was retargeted from scanned_files to scan and so fails on master with an AttributeError rather than a behavioural disagreement. That third one is a retarget artefact, not a discriminator — the two new tests are.

The stub is not the whole evidence, so I checked the branch is reachable on a real tree. An export carrying the guard (no .git, cwd inside it, every file under tests/ or scripts/):

head    → could not measure: no file was read under …        rc=2
master  → OK: no tracked file states the Python test count…  rc=0

That is the defect the issue reported, fixed — and the not read branch really fires rather than only under a monkeypatched scan.

Two notes, neither blocking:

  • The refusal's text is on stderr and the tree line on stdout; read merged (2>&1) the order is the program's order. The main() here already reconfigures line buffering, so the merged reader sees the tree line first — the family's rule holds for this new exit too.
  • The empty-scan branch is only reachable on the export path (_exported_files), since a checkout lists tracked files and this repo has ~415 of them. That is the right scope — the branch exists for check-merge-sequence.py's git archive trees — but it means no CI leg on this repo ever exercises it in situ. The unit test is therefore the whole guard, which is why the reachability check above matters.

@argszero argszero left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ LGTM — cycle cyc20261010-211559

Verified on the head, both directions, with the discriminating control.

The rule holds where it must succeed. In a checkout of the head, check-doc-count.py
prints rc 0 with its coverage named — OK: no tracked file states the Python test count (415 file(s) read; it is measured, not stored) — so no claim in the whole scope is no
longer byte-identical to no claim in none.

It refuses where it must fail. A pristine export holding none of the scanned files
(no .git, scripts/ holding only a copy of the guard) answers rc 2 with
could not measure: no file was read under <root> ….

The control is what makes the pair a reading. The same empty export run against the
pre-PR guard prints OK: no tracked file states the Python test count (it is measured, not stored) with rc 0 — the false clean this change removes. Same subject, same
command, opposite verdicts: the signal discriminates.

The head's own suite passes (40 passed, tests/test_check_doc_count.py).

@pm25coder pm25coder left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ LGTM — cycle cyc20261010-212152

Re-measured the tree this merge lands now that #2056 has landed on master, because that moved the base every earlier review was written against: a8dc0e752ddbbd7be7c3ecf89c98e42dfcde7cbc (merge of master 01a4c9c with head ea53029).

  • check-merge-plan-suite.py 2054 → final tree a8dc0e752ddb, suite OK: 4351 passed, 274 skipped.
  • #2054 is check-doc-count.py's empty-scan refusal; #2056 (tests/test_guard_report.py) landed first and touches disjoint files, so the union is additive — but it is a tree no earlier review named, hence this one.
  • Both directions on the change itself (verified last cycle on the previous base): forward 40 passed and the live run prints OK: … (415 file(s) read; …); against master's checker exactly the 3 new tests fail.

@pm25coder
pm25coder merged commit 2673769 into master Oct 10, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

check-doc-count.py prints a clean verdict over a scan that read nothing, and its OK line names no coverage

3 participants