Skip to content

fix(coverage): narrow parse-error ranges, and stop the report hiding what it dropped (#963) - #1941

Open
CaptainMittens wants to merge 4 commits into
DeusData:mainfrom
CaptainMittens:fix/parse-coverage-honest-ranges
Open

fix(coverage): narrow parse-error ranges, and stop the report hiding what it dropped (#963)#1941
CaptainMittens wants to merge 4 commits into
DeusData:mainfrom
CaptainMittens:fix/parse-coverage-honest-ranges

Conversation

@CaptainMittens

Copy link
Copy Markdown

What this fixes

index_status reported src/cli/cli.c as one error range of 1-13047 — the
whole file. The file was indexed fine; the report was wrong, and "grep 13,000
lines" is not useful advice.

The cause is conditional compilation splitting a brace. Tree-sitter parses raw
text with no preprocessor, so this shape gives it two if headers and one
closing brace:

#ifndef _WIN32
if (...) {
#else
if (...) {
#endif

At file scope it cannot recover, the root node becomes an error node, and the
old code blamed the whole file. cli.c has three such blocks. This is not
specific to us — a platform guard around lstat/stat is a common shape in C.

The fix reads the SECOND parse. The pipeline already preprocesses any file
containing #ifndef, and that expanded parse is clean. The report simply never
consulted it.

Measured result

file before after share of file
src/cli/cli.c 1-13047, one range 85 ranges, 1819 lines 100% → 13.9%
tests/test_cli.c 6821 lines to EOF 70 ranges, 441 lines 48.6% → 3.1%
src/cli/activation_transaction.c 907 lines to EOF 11 ranges, 175 lines 38% → 7.5%
whole repo 78 flagged files 58 flagged files

What survives is honest. The three biggest ranges left in cli.c are real
discarded platform blocks that are genuinely absent from the graph on this
platform.

Two silent caps, both fixed

Narrower ranges mean more of them, which pushed real files against limits that
used to be unreachable. There were two, in series, and both dropped ranges with
no signal at all:

cap where was now
CBM_MAX_ERROR_REGIONS internal/cbm/cbm.c 64 256
COVERAGE_RANGE_MAX src/mcp/mcp.c 128 256

Both were measured binding at 64 — cli.c and tests/test_cli.c sat on
exactly 64 ranges each. That means every coverage number this project has ever
reported for those files was a floor, not a measurement.

A raised cap is still a cap, so a range string can now end with ,+<N> naming
how many ranges were dropped, and check_index_coverage reports
"truncated": true — both when it sees that marker and when its own limit
stops the list.

A third coverage kind: parse_unusable

A file whose single range still covers 80 percent or more of it now reports
parse_unusable instead of listing the range. The file WAS indexed; pointing a
reader at nearly every line just tells them nothing, so the report says "read
the source directly".

Its main customer is not C. The narrowing step only runs for C, C++ and CUDA,
so a Python, Java or Ruby file whose root node fails still reports one
whole-file range. Verified against real broken files in those languages.

It is deliberately not called parse_failed. "Failed" reads as a skip phase,
and a reader who thinks a file was skipped believes it is absent from the graph
entirely. Two places in the report already fell through to "skipped" for want
of an explicit branch; both now name the kind.

New CI gate

scripts/ci/self-index-coverage-gate.sh indexes this repo with the built
binary and fails the PR on any of:

  • a file in the parse_unusable class
  • any error_ranges string carrying a +<N> truncation marker
  • a single range covering more than 25% of a file of 200 lines or more
  • parse_partial_count above the checked-in ceiling (58 today)

It runs in the existing pr-smoke job, Ubuntu leg only, which is already a
required check. Every one of the four checks was verified to FAIL, not just to
pass — with an emptied allowlist, a lowered threshold, a lowered ceiling, a
repo of deliberately broken files, and a 1200-line garbage file.

scripts/ci/coverage-gate-allowlist.txt carries exactly one entry:
scripts/setup-windows.ps1, whose single range is 25.5% of its file. That is a
real gap — the tree-sitter PowerShell grammar cannot parse a } else { branch
running to EOF — so it is allowlisted with the reason written beside it rather
than hidden by raising the threshold. Every other file of 200+ lines sits at
3.9% or below.

Tests

Every test was checked RED before it was kept, either by breaking the code
under it or by measuring first.

  • The range narrows to the dropped branch; explained lines are excluded; a
    range never starts or ends on a directive; real garbage beside a split brace
    is still flagged; a clean file stays unflagged.
  • The cap reports what it dropped, and an under-cap file carries no marker.
  • The three coverage classes are told apart, including a non-C whole-file case.
  • The Studio Export range join puts ONE marker at the end with the summed
    count — a marker left mid-string makes every reader stop there and silently
    lose the ranges after it.
  • check_index_coverage emits every range in front of a marker, never turns
    the marker's digits into a range, and reports truncated from both caps.
  • tests/test_index_resilience.c gained a ceiling beside its floor: exactly
    one of two fixture files is flagged and the clean neighbour is absent.
  • The three _Thread_local grammar forms are pinned as measured, so a
    tree-sitter bump that changes them turns a test red instead of leaving a
    stale note behind.

Full suite: 7735 passed, 28 failed, 7 skipped. The 28 are pre-existing
agent-client install/uninstall failures in the cli suite — a clean worktree
at main reports the identical 263/28, so they belong to a separate ticket.

Two things found on the way, filed not fixed

  • scripts/setup-windows.ps1 reports 113-113,113-113,245-327: the same range
    twice, and an end line past the end of a 326-line file.
  • scripts/smoke-invariants.sh has a stale EXPECTED_TOOLS list that omits
    check_index_coverage and compare_graphs.

Related to #963.

🤖 Generated with Claude Code

https://claude.ai/code/session_01VgDHuhXmjdrwzowPN68wsC

CaptainMittens and others added 4 commits August 29, 2026 23:48
…eusData#963)

src/cli/cli.c reported an error range of 1-13047 — the whole file. The file
indexed fine; the report was wrong. Three #ifndef _WIN32 blocks split a brace
(two `if` headers, one closing brace), so the raw tree-sitter parse cannot
resync at file scope, the root node becomes ERROR, and cbm.c takes its
whole-file branch.

The pipeline already parses these files a second time after preprocessing, and
that parse is clean. The report just never consulted it.

Build one byte per original line from the preprocessed pass, then cut each raw
error range down to the runs of lines the second parse could not vouch for.

Three rules, all found by running it and all load-bearing:

- An expanded line only vouches for its original line when it HAS TEXT. The
  preprocessor emits a blank line where it dropped a branch; treating that
  blank as proof suppressed every C range in the suite.
- Preprocessor directive lines (with backslash continuations) never count as
  missing code — the preprocessor consumes them, so the second parse can never
  vouch for one. Without this every #include block reported as a miss. Known
  cost: a #define the raw parse really dropped no longer shows up on its own.
- A TOP-LEVEL macro invocation line never counts as vouched-for even when the
  expanded line parses clean. The macro can expand to a whole definition that
  the recovery walker deliberately refuses to adopt (DeusData#949), so a clean second
  parse there proves nothing. An in-body invocation is the benign DeusData#1071 case
  and is left to the existing macro subtraction.

The order of the three coverage steps is now settled by where each one's
evidence lives:

  recovery subtraction  -> before the refinement; its evidence is a whole
                           definition that STARTS inside the range, so it must
                           be asked while the range still matches the construct
  the refinement        -> middle
  DeusData#1071 macro rule      -> after the refinement; its evidence is per-line, so a
                           narrow range points at the call itself

Measured on this repo: src/cli/cli.c goes from one whole-file range to 64
ranges over ~9.8% of the file, tests/test_cli.c from 48.6% to ~2.9%,
src/cli/activation_transaction.c from 38% to 7.5%. What survives is honest —
the biggest remaining ranges in cli.c are genuinely discarded #ifdef _WIN32
and #ifdef CBM_CLI_ENABLE_TEST_API blocks, absent from the graph on this
platform.

Both percentages above are floors, not measurements: cli.c and test_cli.c now
land on exactly 64 ranges, which is CBM_MAX_ERROR_REGIONS. That cap drops
regions with no signal, and a follow-up raises it and adds a truncation marker.

Five tests, all red before the change: the range narrows to the dropped
branch; lines the preprocessor explained are excluded; a range never starts or
ends on a directive; real garbage beside a split brace stays flagged; a clean
file stays unflagged.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VgDHuhXmjdrwzowPN68wsC
…-file failures (DeusData#963)

Two silent failures in the parse-coverage report, both made visible by the
Phase 2 range refinement that came before this.

## The caps dropped ranges with no signal

Two caps sat in series and both returned early without saying anything:

    CBM_MAX_ERROR_REGIONS = 64   internal/cbm/cbm.c
    COVERAGE_RANGE_MAX   = 128   src/mcp/mcp.c

Raising only the first would have moved the clip from 64 to 128, so both move
to 256. This was live behaviour, not a theoretical limit: after Phase 2 split
one whole-file range into many small ones, src/cli/cli.c and tests/test_cli.c
both reported exactly 64 ranges — the cap binding, dead-on, twice. Every
coverage figure measured before this change was a floor. With the cap at 256
the true numbers are cli.c 13.9% (not 9.8%) and test_cli.c 3.1%, and the
longest list in the repo is 85 ranges.

A raised cap is still a cap, so the report now says when it clipped:

- cbm_error_regions_t gained a `dropped` counter, and cbm_collect_error_regions
  walks to the end instead of stopping at the cap, so the count is exact rather
  than a lower bound. That costs little — the walk never descends into an ERROR
  subtree.
- cbm_error_ranges_str appends ",+<N>" when N ranges were thrown away.
- coverage_add_ranges reads that marker and sets "truncated": true, and also
  sets it when its own limit stops the loop. Before this the marker was
  invisible: the parser stopped at the '+' with no error and no leftover, so a
  clipped list arrived looking complete.
- objectscript_export_append_error_ranges strips markers off both operands
  before joining two Studio Export parts and adds one back at the end. A marker
  left mid-string would make every reader stop there and silently lose every
  range after it.

## A whole-file range is not advice

"Look at lines 1 to 13047" of a 13046-line file tells a reader nothing. Those
files now carry their own kind rather than being described as partially
covered.

New `parse_unusable` field in CBMFileResult, set when one range covers 80% or
more of the file. Its customers are non-C languages: the Phase 2 refinement
that narrows a whole-file range using the preprocessed parse only runs for C,
C++ and CUDA, so a Python, Java, Ruby or TypeScript file whose root node is
ERROR still reports 1-N. Verified against real files in all four.

The kind is `parse_unusable`, not `parse_failed`. index_coverage.kind already
means one of two things — indexed-but-partial, or a skip phase saying the file
was never indexed at all — and `parse_failed` reads as the second when it is
the first. The store.c schema comment, which is the only written record of this
vocabulary, now describes all three classes and says why.

Two places would have mislabelled the new kind as "skipped", which is exactly
that confusion: coverage_status fell through to its catch-all pass, and
add_coverage_report fell into its else branch. A reader who finds a file under
"skipped" believes it is absent from the graph, when it was indexed. Both now
have explicit branches. index_status gained parse_unusable_count so a CI gate
can read it without parsing anything else, get_code_snippet says "read the
source directly" instead of naming useless ranges, and the three tool
descriptions that listed two coverage kinds now list three.

## Tests

Seven added. The cap test moved from 64 to 256; a new test asserts the marker
carries a real drop count and that nothing follows it; an inverse test asserts
an under-cap file carries no marker at all. For the new kind: a Python file
whose root is ERROR is unusable, a file with a local parse failure stays
partial, a clean file is neither, and — the one that matters most — the
#ifdef-split C file that started this work is partial and never unusable. If
that last one ever flips, the Phase 2 refinement has stopped working.

Full suite: 7732 passed, 28 failed, 7 skipped. The 28 are pre-existing
agent-client install/uninstall failures in the cli suite, identical in count
and identity at clean HEAD.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VgDHuhXmjdrwzowPN68wsC
…es bad (DeusData#963)

A coverage range is advice — "these lines are missing from the graph, read
them". It stops being advice when it names most of the file, and it stops
being honest when the list was clipped without saying so. Both happened here:
src/cli/cli.c reported its whole 13,046 lines as one range, and two caps in
series dropped ranges with no signal. Nothing would have caught either.

scripts/ci/self-index-coverage-gate.sh indexes this repo with the binary just
built and fails on any of four things:

1. A file reports a whole-file parse failure (parse_unusable). Zero today.
2. Any range string carries the "+<N>" truncation marker. With the cap at 256,
   a file that still overflows is worth stopping for.
3. Any single range covers more than 25% of its file, for files of 200 lines
   or more. The floor matters: a 5-line PL/SQL limitation fixture with a 3-line
   range is 60% of itself and says nothing about report quality.
4. parse_partial_count rises above the ceiling in parse-partial-baseline.txt
   (58 today). This complements the FLOOR in tests/test_index_resilience.c,
   which stops the signal being switched off by accident.

Every check was verified to FAIL, not just to pass:

  empty allowlist        -> setup-windows.ps1 flagged at 25.5%
  MAX_SINGLE_RANGE_PCT=3 -> cli.c flagged at 3.9%
  ceiling 57             -> parse_partial_count 58 flagged
  a repo of broken files -> 4 whole-file failures flagged
  a 1200-line garbage file -> its clipped range list flagged

scripts/setup-windows.ps1 is the one allowlist entry, and it is a real gap
rather than noise: one range covers lines 245-327 of a 326-line file because
the tree-sitter PowerShell grammar cannot parse the `} else {` branch running
to EOF, so those 83 lines genuinely are absent from the graph. Every other
file of 200+ lines sits at 3.9% or below, so the 25% threshold has room and
should not be raised to hide this.

Wired into the existing pr-smoke job, Ubuntu leg only. That job is already in
ci-ok's needs, so the gate is a required check with no workflow-graph surgery.
Ubuntu only because the flagged ranges depend on which conditional-compilation
branches the preprocessor keeps — on a machine where _WIN32 is defined a
different set of lines is flagged, which is why the gate asserts proportions
and never exact line numbers. The changes filter now notices edits to the gate,
the allowlist and the baseline. Runs in 21 seconds.

Not extended into scripts/smoke-invariants.sh on purpose: that runs from
smoke.yml, whose triggers are workflow_dispatch and push to qa/smoke-**, and
which is documented non-gating — it would never run on a PR.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VgDHuhXmjdrwzowPN68wsC
…e grammar limit (DeusData#963)

Phase 5. Four test groups, each checked RED before it was kept.

- The Studio Export range join puts ONE ",+<N>" marker at the end with the
  summed drop count. A marker left mid-string makes every reader stop there
  and silently lose the ranges after it. Reaching the join through the
  pipeline needs an export file with 256+ error regions across two <Class>
  elements, so it goes through a test seam, following the pattern already in
  this repo (CBM_COVERAGE_MARKER_TEST_API).
- check_index_coverage emits every range in front of a marker, never turns
  the marker's digits into a range, and reports "truncated" from BOTH caps —
  the producer's and its own 256 limit.
- test_index_resilience now has a ceiling beside its floor: exactly one of
  the two fixture files is flagged, the clean neighbour is absent, and the
  range does not cover the whole file.
- The three _Thread_local forms are pinned as measured. Only the array form
  fails today; the plan's Phase 0 also listed the pointer form, and that is
  wrong on the grammar shipped now.

Also fixes 13 clang-format violations the earlier commits on this branch left
in cbm.c, mcp.c and pass_definitions.c. `make -f Makefile.cbm lint-format`
would have failed CI. The changes are whitespace only — the two reflowed tool
descriptions concatenate byte-identically, so no output moved.

Full suite: 7735 passed, 28 failed, 7 skipped. The 28 are the pre-existing
cli install/uninstall failures, identical at clean HEAD.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VgDHuhXmjdrwzowPN68wsC
@github-actions

Copy link
Copy Markdown

Thanks for opening this — it has been seen, and it is queued.

This note is automated, but it is not a brush-off: it exists so you know where your PR stands instead of having to guess from silence.

Current review status: working through a backlog. 0.9.1-rc.1 is out, so the release freeze that held reviews is over — but it left a large queue of open pull requests behind it, and we are reading through them oldest-first. The background is in discussion #1144.

What that means for this PR, concretely:

  • It will not be closed for inactivity. No stale bot touches pull requests here.
  • It may still sit a while before a human reads it. That is on us, not on you.
  • Older PRs are read first, so a recent one is not being skipped — it is behind a queue.

Things that will genuinely speed it up whenever review does happen:

  • Keep it rebased on main — the tree is moving quickly right now, and a conflicting branch cannot be reviewed as the diff you intended.
  • Get CI green, or say which failures you believe are pre-existing.
  • Keep the change to one claim. Bundled features and refactors get split before they get merged, which costs you a round trip.
  • Every commit needs a sign-off (git commit -s) — CI enforces DCO.

If this fixes a bug, a reproduction we can run is worth more than a description of the symptom.

Thanks for contributing, and sorry in advance for the wait.

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.

1 participant