Skip to content

Commit fccf821

Browse files
ci(coverage): fail a PR when this repo's own parse-coverage report goes bad (#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
1 parent db2e76f commit fccf821

4 files changed

Lines changed: 150 additions & 1 deletion

File tree

.github/workflows/pr.yml

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ jobs:
5959
# The paginated files endpoint remains filename-only for this gate.
6060
FILES=$(gh api --paginate "repos/$REPO/pulls/$PR/files?per_page=100" --jq '.[].filename')
6161
printf '%s\n' "$FILES"
62-
if printf '%s\n' "$FILES" | grep -qE '^(src/|internal/|install\.(sh|ps1)|scripts/build\.sh|scripts/smoke-test\.sh|scripts/smoke-local\.sh|scripts/smoke-fixture-server\.py|scripts/gen-third-party-notices\.sh|scripts/env\.sh|test-infrastructure/vm/(vm-smoke\.sh|windows-user-path-guard\.ps1)|Makefile\.cbm)'; then
62+
if printf '%s\n' "$FILES" | grep -qE '^(src/|internal/|install\.(sh|ps1)|scripts/build\.sh|scripts/smoke-test\.sh|scripts/smoke-local\.sh|scripts/smoke-fixture-server\.py|scripts/gen-third-party-notices\.sh|scripts/env\.sh|scripts/ci/(self-index-coverage-gate\.sh|coverage-gate-allowlist\.txt|parse-partial-baseline\.txt)|test-infrastructure/vm/(vm-smoke\.sh|windows-user-path-guard\.ps1)|Makefile\.cbm)'; then
6363
echo "product=true" >> "$GITHUB_OUTPUT"
6464
else
6565
echo "product=false" >> "$GITHUB_OUTPUT"
@@ -127,6 +127,15 @@ jobs:
127127
CCACHE_DIR: ${{ github.workspace }}/.ccache
128128
CCACHE_MAXSIZE: 1000M
129129

130+
# Index this repo with the binary just built and check its own
131+
# parse-coverage report is still useful advice (#963). Ubuntu only: the
132+
# flagged line ranges depend on which conditional-compilation branches
133+
# the preprocessor keeps, so they differ per platform. The gate asserts
134+
# proportions, never exact line numbers.
135+
- name: Parse-coverage gate (Ubuntu)
136+
if: matrix.os == 'ubuntu-latest'
137+
run: scripts/ci/self-index-coverage-gate.sh "$(pwd)/build/c/codebase-memory-mcp"
138+
130139
- name: Build prod + smoke (macOS)
131140
if: matrix.os == 'macos-14'
132141
run: |
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
# Files the self-index coverage gate skips, one repo-relative path per line.
2+
#
3+
# Adding a line here is a deliberate decision, not a convenience. It says:
4+
# "we know this file reports a wide parse-coverage range, we have looked at
5+
# why, and we accept it." Write the reason above the path. A line with no
6+
# reason should be removed rather than trusted.
7+
#
8+
# Blank lines and lines starting with # are ignored.
9+
10+
# One range covers lines 245-327 of a 326-line file (25.5%). The tree-sitter
11+
# PowerShell grammar cannot parse the `} else {` branch that runs to the end
12+
# of the file, so those 83 lines really are absent from the graph. This is a
13+
# genuine grammar gap, not a reporting error. Every other file of 200+ lines
14+
# in this repo sits at 3.9% or below.
15+
scripts/setup-windows.ps1
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
# Ceiling for parse_partial_count when this repo indexes itself.
2+
#
3+
# The number below is what the gate allows. It complements the FLOOR asserted
4+
# in tests/test_index_resilience.c, which stops the signal being switched off
5+
# by accident. Raising this number is allowed but should be explained in the
6+
# commit that does it.
7+
58
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
#!/usr/bin/env bash
2+
# Regression guard: this repo's own parse-coverage report must stay useful.
3+
#
4+
# A coverage range is advice — "these lines are missing from the graph, read
5+
# them". Advice stops being advice when it names most of the file, and it
6+
# stops being honest when the list was clipped without saying so. Both things
7+
# happened here before (#963): src/cli/cli.c reported its whole 13,046 lines
8+
# as one range, and two caps in series dropped ranges with no signal at all.
9+
#
10+
# This indexes the repo with a given binary and fails if any of that comes back.
11+
#
12+
# Usage: self-index-coverage-gate.sh <path-to-codebase-memory-mcp-binary>
13+
#
14+
# NOTE ON PLATFORM: the ranges depend on which conditional-compilation branches
15+
# the preprocessor keeps. On a machine where _WIN32 is defined the discarded
16+
# branches swap and a different set of lines is flagged. That is why this runs
17+
# on ONE CI leg and asserts proportions rather than exact line numbers.
18+
set -euo pipefail
19+
20+
BIN="${1:?usage: self-index-coverage-gate.sh <path-to-binary>}"
21+
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
22+
ALLOWLIST="${REPO_ROOT}/scripts/ci/coverage-gate-allowlist.txt"
23+
BASELINE_FILE="${REPO_ROOT}/scripts/ci/parse-partial-baseline.txt"
24+
25+
# Share of a file one range may cover before it stops being useful advice.
26+
# The worst real offender today is src/cli/cli.c at 3.9%, so this has room.
27+
MAX_SINGLE_RANGE_PCT="${MAX_SINGLE_RANGE_PCT:-25}"
28+
# Files below this are exempt: a 5-line fixture with a 3-line range is 60% of
29+
# itself and says nothing about report quality.
30+
MIN_FILE_LINES="${MIN_FILE_LINES:-200}"
31+
32+
command -v jq >/dev/null || { echo "FAIL: jq is required"; exit 1; }
33+
34+
WORK="$(mktemp -d)"
35+
# The runtime dir holds a unix socket, and a socket path has a hard length
36+
# limit (~104 bytes). macOS puts mktemp under /var/folders/<long>/T/, which
37+
# blows that limit and fails with "secure CLI coordination could not be
38+
# created (endpoint)". Keep the runtime dir short and separate from the cache.
39+
RUNTIME="/tmp/cbm-gate.$$"
40+
trap 'rm -rf "$WORK" "$RUNTIME"' EXIT
41+
export CBM_CACHE_DIR="${WORK}/cache"
42+
export CBM_RUNTIME_DIR="$RUNTIME"
43+
mkdir -p "$CBM_CACHE_DIR" "$RUNTIME"
44+
45+
echo "==> indexing ${REPO_ROOT} with $(basename "$BIN")"
46+
"$BIN" cli index_repository --repo-path "$REPO_ROOT" --mode full --json \
47+
> "${WORK}/index.json" 2>"${WORK}/index.err" || {
48+
echo "FAIL: index_repository exited non-zero"; tail -20 "${WORK}/index.err"; exit 1; }
49+
50+
PROJECT="$(jq -r '.structuredContent.project // empty' "${WORK}/index.json")"
51+
[ -n "$PROJECT" ] || { echo "FAIL: index_repository did not name a project"; exit 1; }
52+
53+
"$BIN" cli index_status --project "$PROJECT" --json > "${WORK}/status.json" 2>/dev/null || {
54+
echo "FAIL: index_status exited non-zero"; exit 1; }
55+
56+
# Allowlisted paths, comments and blanks stripped.
57+
ALLOWED="${WORK}/allowed.txt"
58+
: > "$ALLOWED"
59+
[ -f "$ALLOWLIST" ] && sed -e 's/#.*//' -e 's/[[:space:]]*$//' "$ALLOWLIST" \
60+
| grep -v '^$' > "$ALLOWED" || true
61+
62+
FAILURES=0
63+
note_failure() { echo "FAIL: $*"; FAILURES=$((FAILURES + 1)); }
64+
65+
# ── 1. Nothing may fail across a whole file ────────────────────────────────
66+
# parse_unusable means one range covers 80%+ of the file, so the report tells
67+
# a reader to go read the source. Zero today; a new one is a real regression.
68+
UNUSABLE="$(jq -r '.structuredContent.parse_unusable.count // 0' "${WORK}/status.json")"
69+
UNUSABLE_LISTED="$(jq -r '[.structuredContent.parse_unusable.files[]?.path]|join(" ")' \
70+
"${WORK}/status.json")"
71+
for p in $UNUSABLE_LISTED; do
72+
grep -qxF "$p" "$ALLOWED" && UNUSABLE=$((UNUSABLE - 1))
73+
done
74+
if [ "$UNUSABLE" -gt 0 ]; then
75+
note_failure "${UNUSABLE} file(s) report a whole-file parse failure: ${UNUSABLE_LISTED}"
76+
fi
77+
78+
# ── 2. No range list may be silently clipped ──────────────────────────────
79+
# A trailing "+<N>" says the producer's cap threw N ranges away. With the cap
80+
# at 256 a file that still overflows is worth stopping for.
81+
TRUNCATED="$(jq -r '[.structuredContent.parse_partial.files[]?
82+
| select(.error_ranges? // "" | test("\\+[0-9]+$")) | .path] | join(" ")' \
83+
"${WORK}/status.json")"
84+
for p in $TRUNCATED; do
85+
grep -qxF "$p" "$ALLOWED" && continue
86+
note_failure "$p carries a +N truncation marker — its range list was clipped"
87+
done
88+
89+
# ── 3. No single range may cover a quarter of its file ────────────────────
90+
while IFS=$'\t' read -r path ranges; do
91+
[ -n "$path" ] || continue
92+
grep -qxF "$path" "$ALLOWED" && continue
93+
[ -f "${REPO_ROOT}/${path}" ] || continue
94+
total="$(wc -l < "${REPO_ROOT}/${path}" | tr -d ' ')"
95+
[ "$total" -ge "$MIN_FILE_LINES" ] || continue
96+
widest="$(printf '%s' "$ranges" | tr ',' '\n' | grep '^[0-9]' \
97+
| awk -F- '{d=$2-$1+1; if (d>m) m=d} END {print m+0}')"
98+
pct="$(awk -v a="$widest" -v b="$total" 'BEGIN{printf "%.1f", 100*a/b}')"
99+
over="$(awk -v p="$pct" -v lim="$MAX_SINGLE_RANGE_PCT" 'BEGIN{print (p>lim)?1:0}')"
100+
if [ "$over" = "1" ]; then
101+
note_failure "$path has one range of ${widest} lines — ${pct}% of ${total}, over ${MAX_SINGLE_RANGE_PCT}%"
102+
fi
103+
done < <(jq -r '.structuredContent.parse_partial.files[]?
104+
| "\(.path)\t\(.error_ranges // "")"' "${WORK}/status.json")
105+
106+
# ── 4. The flagged-file count must not drift upward unnoticed ─────────────
107+
CEILING="$(sed -e 's/#.*//' "$BASELINE_FILE" | grep -oE '[0-9]+' | head -1)"
108+
PARTIAL="$(jq -r '.structuredContent.parse_partial.count // 0' "${WORK}/status.json")"
109+
if [ "$PARTIAL" -gt "$CEILING" ]; then
110+
note_failure "parse_partial_count is ${PARTIAL}, above the ceiling ${CEILING} in $(basename "$BASELINE_FILE")"
111+
fi
112+
113+
echo "==> parse_partial=${PARTIAL} (ceiling ${CEILING}) parse_unusable=${UNUSABLE} allowlisted=$(wc -l < "$ALLOWED" | tr -d ' ')"
114+
if [ "$FAILURES" -gt 0 ]; then
115+
echo "FAIL: ${FAILURES} coverage-gate check(s) failed"
116+
exit 1
117+
fi
118+
echo "PASS: parse-coverage report is within bounds"

0 commit comments

Comments
 (0)