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