Skip to content

ci(coverage): fail a PR when this repo's own parse-coverage report goes bad (#963) - #1968

Open
CaptainMittens wants to merge 9 commits into
DeusData:mainfrom
CaptainMittens:ci/self-index-coverage-gate
Open

ci(coverage): fail a PR when this repo's own parse-coverage report goes bad (#963)#1968
CaptainMittens wants to merge 9 commits into
DeusData:mainfrom
CaptainMittens:ci/self-index-coverage-gate

Conversation

@CaptainMittens

@CaptainMittens CaptainMittens commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

A CI gate that fails a pull request when this repository's own parse-coverage report stops being useful advice (#963). It indexes the merge base and the branch with the binary just built, and fails only on a finding the branch added.

The precondition from #1985 is now met

You wrote:

the gate is accepted in principle as a required check once merge-base comparison is included, so unrelated pull requests do not fail merely because main moved the baseline

and:

with a constant ceiling, a legitimate language-coverage improvement reads as a gate failure on an unrelated PR, and the person who hits it has no way to tell that from a real regression

All four checks are now differential. The gate resolves the base commit, checks it out into a temporary worktree, indexes both trees with the same binary, and compares:

# Check Fails when
1 whole-file parse failure (parse_unusable) the path is flagged on this branch and not at the merge base
2 a +N clipping marker on a range list the marker is new at head
3 one range over 25% of a file of 200+ lines the file was within the share at the merge base
4 parse_partial_count it is above the merge base's count

scripts/ci/parse-partial-baseline.txt stops being a gate. The script still prints the recorded number so a reader can see the drift, and says in the output that nothing fails on it. Nobody has to raise that number again — which is what #1972 was, and what #1824 would have been. Blazor .razor files map to C#, their markup lands in ERROR regions by design, and parse_partial_count rises by roughly the repository's .razor count. Under the old gate that coverage improvement read as a failure on an unrelated branch.

Every run prints both sides, so a failure reads as "the base reports X, this branch reports Y" rather than as a bare number:

==> base 5fbab7bb7332bd06aaa880653ddfb2696e648f90: parse_partial=59 unusable=0
==> head:            parse_partial=59 unusable=0
==> allowlisted=1 recorded=59 (record only, not enforced)
PASS: this branch did not make the parse-coverage report worse than the merge base

What this gate cannot see, stated plainly

Both trees are indexed with the same binary, so the comparison isolates what the tree changed. A pull request that changes the extractor itself moves the base side and the head side together, and this gate will not fail on it.

Catching that needs the base commit's own binary, which means a second full build. In job 99872656120 the build step took 11m49s and this whole gate step took 26s (13:34:32 to 13:34:58), so a second binary costs about 27x what the comparison costs. That is not a trade I wanted to make silently, so I did not make it. Two things still cover the case: the FLOOR asserted in tests/test_index_resilience.c stops the coverage signal being switched off, and the absolute counts for both sides now print on every run, so a jump is visible in the log even when it does not fail. The script header and the scripts/ci/README.md row both say this.

If you would rather have the second build, say so and I will add it — it is a workflow change, and those are yours to call.

One check stays absolute, on purpose

index_status lists at most 500 files per class and sets truncated when it drops the rest. Checks 1 to 3 read that list file by file, so a clipped list means they judged only part of the tree and would still print PASS. That is the same silent clipping this gate exists to catch, so a clipped list stops the run on either side rather than being compared.

Tests

tests/test_coverage_gate_contract.sh drives the production script with a fake $BIN — a small shell script that prints canned JSON — so no seam is added to the script itself and no indexing happens. Same stubbing shape as tests/repro/repro_script_summary.sh. It builds a two-commit fixture repository so the base worktree is real.

Fifteen cases: each of the four findings present at both sides (must pass) and new at head (must fail); the count equal to, below, and above the base; the recorded number not gating; both sides printing; a clipped file list still stopping the run; the allowlist skipping a path on both sides; an unresolvable base commit stopping the run; and --help describing the comparison.

Red-green: I reverted each of the four comparisons one at a time and confirmed the matching "present at both sides" case goes red, then restored it. A gate test that only ever passes proves nothing.

ok: unusable at both sides is not this branch's doing
ok: unusable new at head fails
ok: unusable gone at head passes
ok: +N marker at both sides passes
ok: +N marker new at head fails
ok: a range already over the share at base passes
ok: a range newly over the share fails
ok: count equal to base passes
ok: count below base passes
ok: count above base fails
ok: the recorded number does not gate
ok: a clipped file list at head fails
ok: an allowlisted path newly over the share passes
ok: an unreachable base commit stops the run
ok: --help describes the merge-base comparison
PASS: coverage-gate contract

Runs from scripts/test.sh as Step 0y. A real end-to-end run against a built binary passes and takes 45s for two indexes, against 26s for the one index the gate used to do.

Where the base commit comes from

In order: $COVERAGE_GATE_BASE_SHA, then the first parent of HEAD when HEAD is a merge commit, then git merge-base origin/main HEAD. pr.yml now passes COVERAGE_GATE_BASE_SHA: ${{ github.event.pull_request.base.sha }}, so CI uses the commit GitHub itself used to build the merge rather than relying on the fallback. The checkout is shallow, so the script fetches the base commit with --depth=1 when the object is not present.

A note on the commit list

ci(coverage): raise the parse-partial ceiling to 59 is still in this branch, and the commit after it removes the ceiling. I kept both rather than rewriting history: the first commit is the measurement that shows main moved the number on its own, and the second is the fix for that being able to fail a branch at all. The recorded 59 in the data file is that same measurement, which is why the file keeps it.

This branch stacks on #1941

GitHub cannot base a cross-fork pull request on a branch in the fork, so this pull request shows #1941's four fix(coverage) and test(coverage) commits underneath its own four. Please review those on #1941. When #1941 merges, the list here shrinks on its own.

Refs #963, #1972, #1985

@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.

@CaptainMittens
CaptainMittens force-pushed the ci/self-index-coverage-gate branch from 439167e to 9e95410 Compare September 1, 2026 13:21
@DeusData

DeusData commented Sep 1, 2026

Copy link
Copy Markdown
Owner

Maintainer decision on the gate, since that is what this PR was split out to get.

The gate is accepted in principle. One condition before it becomes a required check: #1972's merge-base comparison has to land here, not as a follow-up.

The reasoning is your own evidence. A required check that compares parse_partial_count against a constant goes red on unrelated PRs the moment main adds a grammar gap — and you demonstrated exactly that during the split, when main took the count 58 → 59 through _Atomic(type) in src/daemon/runtime.c, with nothing on your branch responsible. You measured it three ways to prove where it came from, which is the only reason that is knowable at all. But the next person to hit it will not have your context; they will have a red required check on a PR that did nothing wrong, and no obvious way to tell a real regression from a ceiling that needs bumping.

A gate that cannot distinguish "this PR added a gap" from "main added a gap" is not yet a gate — and a required check whose verdict depends on when you branched is the shape we do not ship. You said as much yourself in #1972 and left it alone because reshaping a gate is the maintainer's call. It is, and this is the call: bring the merge-base comparison into this PR and the gate goes in as required.

Nothing else is being asked. Specifically:

  • The allowlist entry stays. scripts/setup-windows.ps1 at 25.2% is a real grammar gap, documented as one, and your point that the 25% threshold should not be raised to hide it is right. We are taking ownership of that entry as part of accepting the gate rather than treating it as something the PR grants itself.
  • The 25%/200-line shape, proportions-not-line-numbers, and Ubuntu-only are all fine as they stand, and the reasons you gave for each are the right ones.

Three things worth saying plainly

You verified the gate fails, not only that it passes. Three negative controls — ceiling lowered, allowlist entry removed, and the earlier MAX_SINGLE_RANGE_PCT=3 run. A gate proven only to pass is indistinguishable from a gate that never checks, and most contributions of this kind stop at the passing run.

You then found three ways your own gate could have passed without checking. That is a rarer instinct than writing the gate.

And one of those three is a house-wide hazard, not a local slip. sed | grep -oE | head -1 aborting under pipefail because head closes the pipe and the writer takes SIGPIPE is a shape this repo has been bitten by before, and there are still dozens of call sites carrying it elsewhere. Your writeup of it is the clearest statement of the failure mode we have. Thank you for that.

Sequencing

#1941#1971 → this one, as you laid out. #1941 is currently red on three legs from its own 13:21 push — those completed at 14:35, before main broke at ~15:08, so they are real rather than the incident.

Separately: main does not currently compile right now (duplicate struct members merged into type_registry.h; #1993 is the repair). Your green here predates that and is stale rather than wrong. Wait for #1993 before rebasing anything in this stack.

@DeusData DeusData added enhancement New feature or request github_actions Pull requests that update GitHub Actions code maintainer-notes Internal observations and improvement notes parsing/quality Graph extraction bugs, false positives, missing edges priority/normal Standard review queue; useful PR with ordinary maintainer urgency. labels Sep 1, 2026
@DeusData

DeusData commented Sep 1, 2026

Copy link
Copy Markdown
Owner

One interaction to record while the gate design is being reworked, so it is not discovered as a mystery later.

#1824 makes Blazor .razor files discoverable by mapping them to C#. By design those files land in parse_partial: the C# grammar recovers the @code block while the surrounding markup lands in ERROR regions. That is the intended and honest outcome — it is strictly more than the Module-and-imports other markup-hosted languages yield today — but it means parse_partial_count rises by roughly the repo's .razor count when that PR lands.

That rise is correct, not a regression, and it is exactly the case your gate cannot currently distinguish — the same shape you already identified when main took the count 58 → 59 on its own and filed #1972.

It is a second, concrete argument for the merge-base comparison being a precondition rather than a follow-up: with a constant ceiling, a legitimate language-coverage improvement reads as a gate failure on an unrelated PR, and the person who hits it has no way to tell that from a real regression.

Nothing to do here beyond what was already asked. Recording it so the ordering is deliberate.

CaptainMittens and others added 9 commits September 1, 2026 20:06
…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>
Signed-off-by: Joshua Richter <jrichter5781@gmail.com>
…-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>
Signed-off-by: Joshua Richter <jrichter5781@gmail.com>
…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>
Signed-off-by: Joshua Richter <jrichter5781@gmail.com>
…DeusData#963)

Review follow-up on this branch.

Each parse_unusable entry carries one number, and the field was called
"lines". That reads as the length of the file, and the two are not the same
number: a grammar can end an error node past the last line, which this repo
has already met — scripts/setup-windows.ps1 has 326 lines and its range ends
at 327. A report whose whole thesis is honest reporting should not name that
number after the wrong thing.

"lines" also already means something else in this same response. Every search
result carries a "lines" field holding a definition's line span. One word, two
meanings, one document.

The field is now "range_end", at both places that emit it — add_coverage_report
reading the persisted rows, and add_parse_unusable_summary reading the per-run
errors. The comment beside each one says the number can exceed the file, so the
next reader does not have to rediscover it.

Deriving the real file length instead was the other option and is not
available here: neither cbm_file_error_t nor cbm_coverage_row_t carries it,
only the path and the range string.

One test, proved RED first — "range_end is NULL" against the old field name.
It reads the end line from the persisted coverage row rather than a constant,
so it states the property and not a measurement, and it asserts the old name is
gone rather than kept beside the new one.

index_resilience, parse_coverage and mcp: 283 passed, 4 skipped.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Joshua Richter <jrichter5781@gmail.com>
…eusData#963)

scripts/setup-windows.ps1 has 326 lines and its parse-coverage report read
"113-113,113-113,245-327" — the same line named twice, and an end line that
does not exist. Two separate faults, both in cbm_error_regions_push.

Past-EOF end line. A tree-sitter node that ends at column 0 stopped right
after the previous line's newline, so it holds no text on the row it points
at. Adding 1 to that row named a line past the end of the file whenever the
region ran to EOF. The node here is start=(244,2) end=(326,0). Clamp the end
to the row above when the end column is 0 and the node spans more than one
row.

Duplicate range. Line 113 carries two separate ERROR nodes, at columns 25-29
and 31-32, and each pushed its own range. A line range says nothing new the
second time. Drop a range that exactly repeats the one already open. The
drop runs BEFORE the cap check, so a repeat is never miscounted as a range
the cap threw away.

Only an EXACT repeat is dropped, never a range that merely overlaps. Each
range is judged separately afterwards by cbm_region_is_recovered, which asks
whether definitions starting inside that range cover it. Two ranges holding
the same numbers always get the same verdict, so dropping the repeat changes
nothing. Two different ranges do not. Merging 3-3 into 2-3 hands the wider
range's covering definition to an error that definition does not explain, and
a real parse failure then vanishes from the report.

That is not hypothetical. An earlier version of this commit merged on overlap
and broke perl_malformed_source_remains_partial_issue1838, the test added
with the Perl grammar refresh in 17b5a43. The malformed fixture produces two
ERROR nodes, at lines 2-3 and 3-3. Merged, the 2-3 range looks fully covered
by before_error and is removed, so parse_incomplete comes back false on a file
that plainly does not parse. That test now pins this boundary.

The real file reports "113-113,245-326".

Two tests, both proved RED first with the exact expected text:
  coverage_repeated_error_line_reports_one_range_issue963  "2-2,2-2" != "2-2"
  coverage_range_never_ends_past_the_last_line_issue963    "1-5"    != "1-4"

Suites run on this change: parse_coverage 34, extraction 325, pipeline 264,
mcp 246, index_resilience 7 — all passing.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Joshua Richter <jrichter5781@gmail.com>
…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>
Signed-off-by: Joshua Richter <jrichter5781@gmail.com>
…cking (DeusData#963)

Verifying the gate against a locally built binary turned up three defects in
the gate itself, all of the same shape it exists to catch: it could report
PASS without having looked at everything.

Reading the ceiling aborted the whole gate on a long file. The ceiling came
from `sed | grep -oE | head -1`. Under `set -o pipefail` head closes the pipe,
grep dies of SIGPIPE, and the gate exits with no message at all. It does not
bite on today's 7-line baseline file and it does bite on a long one, which is
proved. Replaced with one awk that stops after the first number.

The report's own truncation flag was never read. index_status lists at most
500 files per class (COVERAGE_FILE_CAP) and sets "truncated" when it dropped
the rest. Checks 2 and 3 walk that list file by file, so a clipped list means
they judge part of the repo and still print PASS. New check 0 stops instead.
Today the list holds all 58 files and truncated is false, so this is a guard,
not a fix for live behaviour.

Check 1 named files it had already accepted. It subtracted allowlisted paths
from the count but still printed them in the failure text, so the message
disagreed with the number beside it. It now counts and names the same set.

The gate also did not answer --help, which scripts/ci/README.md says every
script there does. It fed --help to basename, printed a usage error from the
wrong program and exited 0. It now prints a Usage: block and exits 0, rejects
an unknown flag with exit 2 and the house line "Please consult --help.", and
is enrolled in HELP_ENTRIES and STRICT_ENTRIES in the venue parity contract so
the rule is enforced rather than only written down. Breaking --help fails that
contract with exit 1, which is checked.

Added the missing row to the scripts/ci/README.md table.

The allowlist reason for scripts/setup-windows.ps1 carried the old numbers.
After the range fix in the previous commit the file reports 113-113,245-326,
so the widest range is 82 lines rather than 83 — 25.2% of 326. Still over the
25% limit, so the entry stays, and the reason now says why narrowing it by one
line did not clear the gate.

Verified against a locally built binary:
  healthy            PASS, parse_partial=58 (ceiling 58) parse_unusable=0, exit 0
  allowlist emptied  FAIL naming setup-windows.ps1 at 25.2%, exit 1
  ceiling at 57      FAIL naming the count, exit 1
  both restored      PASS, exit 0

Venue parity contract: 19 --help entries, 9 strict-flag entries, green.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Joshua Richter <jrichter5781@gmail.com>
…dded (DeusData#963)

The gate's check 4 failed on its own pull request: parse_partial_count is 59
against a ceiling of 58. The rise did not come from this branch.

Measured with the binary built from this branch, three trees indexed:

  this branch, no merge      58   matches the baseline as written
  origin/main alone          59
  main merged into it        59   identical file list to main alone

CI tests the merge of a pull request into main, so the gate sees 59. The one
file main added to the flagged list is src/daemon/runtime.c, at one line:

  src/daemon/runtime.c   47-47   1 line of 3291   0.03% of the file

Line 47 is a function-style _Atomic declaration:

  static _Atomic(cbm_daemon_runtime_containment_hook_t) runtime_containment_hook_seam;

The tree-sitter C grammar does not parse that form. The keyword form four
lines above it, `static _Atomic uint32_t ...`, parses fine. This is the same
class of grammar limitation this branch already pins for _Thread_local in
tests/test_parse_coverage.c. It arrived with fc1b1ee on main.

So the ceiling moves to 59 rather than the file being allowlisted. An
allowlist entry is for a file whose single range is over the 25% limit and
has a written reason to stay; one line out of 3291 is nowhere near it, and
hiding the file would remove a real gap from the count the ceiling exists to
watch.

Known cost, worth stating plainly: a ceiling checked against a moving main
drifts. Any merge to main that adds a partially-parsing file reddens every
open pull request until someone edits this file by hand, and the pull request
that goes red is never the one that caused it. This commit does not fix that
- the fix is to compare against the merge base instead of a checked-in number,
which changes what the gate is and belongs to whoever owns CI policy here.
Filed separately.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Joshua Richter <jrichter5781@gmail.com>
…eusData#963)

The gate failed a pull request on the state of the report, not on the
change the pull request made. Check 4 compared parse_partial_count
against a number checked into scripts/ci/parse-partial-baseline.txt, and
checks 1-3 asserted zero findings outright. Any of the four could go red
for something main did.

That is not theoretical. DeusData#1972 was this exact thing: main gained
src/daemon/runtime.c, the count went 58 -> 59 on its own, and the number
had to be raised by hand. DeusData#1824 will do it again and larger. Blazor
.razor files map to C#, their markup lands in ERROR regions by design,
and the count rises by roughly the repo's .razor count. A coverage
improvement would read as a gate failure on an unrelated branch, and the
person who hit it would have no way to tell that from a real regression.

The gate now resolves the base commit, checks it out into a temporary
worktree, and indexes both trees with the same binary. All four checks
compare the two:

  1. a whole-file parse failure fails only when it is new at head
  2. a "+N" clipping marker fails only when it is new at head
  3. a range over 25% of its file fails only when the file was within
     the share at the base
  4. the flagged-file count fails only when it is above the base's

parse-partial-baseline.txt stops being a gate. The script still prints
the recorded number so a reader can see the drift, and says plainly that
nothing fails on it. Nobody has to raise that number again.

What this cannot see: both trees are indexed with the same binary, so a
branch that changes the extractor itself moves the base side and the
head side together and this gate will not fail on it. Catching that
needs the base commit's own binary, which means a second full build --
about twelve minutes against the twenty-six seconds the whole gate step
takes. Two things still cover it: the FLOOR asserted in
tests/test_index_resilience.c stops the signal being switched off, and
the absolute counts for both sides now print on every run, so a jump is
visible in the log even when it does not fail. The script header and the
scripts/ci/README.md row both say so.

tests/test_coverage_gate_contract.sh pins the behaviour. It drives the
production script with a fake binary that prints canned JSON, so no seam
is added to the script itself and no indexing happens. Fifteen cases:
each of the four findings present at both sides (pass) and new at head
(fail), the count equal to, below and above the base, the recorded
number not gating, both sides printing, a clipped file list still
stopping the run outright, the allowlist skipping a path, and an
unresolvable base commit stopping the run.

Verified by reverting each of the four comparisons one at a time and
confirming the matching "present at both sides" case goes red, then
restoring. A real run against a built binary passes with both sides
reported, and takes 45s for two indexes.

pr.yml passes COVERAGE_GATE_BASE_SHA so the gate uses the commit GitHub
itself used to build the merge, rather than falling back to the first
parent of HEAD.

Refs DeusData#963, DeusData#1972

Signed-off-by: Joshua Richter <jrichter5781@gmail.com>
@CaptainMittens
CaptainMittens force-pushed the ci/self-index-coverage-gate branch from 9e95410 to a3e8fa3 Compare September 2, 2026 00:19
@CaptainMittens

Copy link
Copy Markdown
Contributor Author

The two red checks are not caused by this branch.

ci-ok is the aggregate and fails only because of test / test-windows-guards
(job 100085897520).

That job stopped in tests\windows\test_daemon_stability.py, after the five sections before it passed:

PASS: parameter surface rejects bare/unknown/out-of-range daemon invocations
PASS: hook fails open without a daemon; notice is visible once, then rate-limited
PASS: start survives an occupied --port, status reports the pid, second start is a no-op
PASS: stop refuses while a session is attached and succeeds once the session closes
PASS: kill -9 recovery — stale state cleared, cold client worked, fresh start got a new pid
SETUP FAIL: permanent daemon did not start for the churn check
RED (tests/windows/test_daemon_stability.py): section_churn_stability failed

The failure is in the setup for the sixth section, not in an assertion.

Three checks against this branch:

Check Result
Files changed here that match daemon or windows none of the 20
Same job on sibling PRs passes on #1986 and #1976
Where the failure sits daemon start for section_churn_stability, not a test the branch touches

I cannot re-run the job — gh run rerun --failed answers Must have admin rights to Repository, which a fork account does not have. Please re-run it when convenient. Nothing on this branch needs to change.

@DeusData

DeusData commented Sep 2, 2026

Copy link
Copy Markdown
Owner

Status check, since this has been sitting: the condition on this PR is still unmet, and it is not something you can clear.

Our maintainer accepted this gate conditional on #1972 landing first — "The coverage gate's parse-partial ceiling drifts, reddening PRs that did not cause it". I checked: #1972 is still open, unclosed. Merging a coverage gate while its known drift defect is unfixed would produce exactly the outcome that issue describes: PRs going red for something they did not cause. So this stays blocked on our side, not yours.

Your two reds are also not yours. test-windows-guards fails on test_daemon_stability.pysection_churn_stability in your run — and ci-ok merely aggregates it.

While attributing it I noticed something worth passing on, because it reframes the whole family. That guard has now gone red on three different sections across unrelated PRs — section_start_status_port, section_crash_recovery, and yours — and every single instance carries the same two lines:

SETUP FAIL: index did not run:
SETUP FAIL: ASCII control did not index via CLI:

The setup failures are common to all three. That suggests the per-section "REGRESSION: a fixed Windows bug is broken again" lines may be downstream of one shared setup fault rather than three independent daemon regressions — which matters, because we have an open PR fixing the section_crash_recovery symptom specifically, and if the setup is the real cause that fix would not clear the others. I have recorded it on our side so it gets chased as one problem rather than three.

Nothing for you to do here. When #1972 lands I will come back to this and re-run.

@CaptainMittens

Copy link
Copy Markdown
Contributor Author

Understood on the block, and I confirmed #1972 is still open on my side too.

On the shared-cause theory, one line from my run that is not in your quote, plus an observation.

My run carries a third setup failure alongside the two you named:

SETUP FAIL: permanent daemon did not start for the churn check
SETUP FAIL: index did not run:
SETUP FAIL: ASCII control did not index via CLI:

I checked #1918's run as well, which is your section_start_status_port instance. It carries your two lines but not the churn one — which fits the theory rather than complicating it. The churn line is that section's own setup step, and the two shared lines sit underneath it.

The observation: SETUP FAIL: index did not run: prints nothing after the colon, in both runs. The guard reports that indexing failed but captures no reason, so the log cannot say why it failed. Further down the same run, test_hook_augment.py is skipped with PRECONDITION ... exit=2, which looks like the same failure carrying onward.

That supports reading these as one setup fault rather than three daemon regressions. A guard whose index never ran cannot show anything about the behaviour it guards, in either direction — so the three "a fixed Windows bug is broken again" verdicts are currently unsupported rather than wrong. Capturing the indexer's own stderr into that empty message would likely name the cause outright.

I have not looked at the section_crash_recovery run, so that third instance is still your reading rather than mine.

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

Labels

enhancement New feature or request github_actions Pull requests that update GitHub Actions code maintainer-notes Internal observations and improvement notes parsing/quality Graph extraction bugs, false positives, missing edges priority/normal Standard review queue; useful PR with ordinary maintainer urgency.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants