|
17 | 17 | # on ONE CI leg and asserts proportions rather than exact line numbers. |
18 | 18 | set -euo pipefail |
19 | 19 |
|
20 | | -BIN="${1:?usage: self-index-coverage-gate.sh <path-to-binary>}" |
| 20 | +usage() { |
| 21 | + cat <<'EOF' |
| 22 | +Usage: scripts/ci/self-index-coverage-gate.sh <path-to-codebase-memory-mcp-binary> |
| 23 | +
|
| 24 | +Index THIS repository with the given binary and fail if the repository's own |
| 25 | +parse-coverage report stops being useful advice (#963). |
| 26 | +
|
| 27 | +Four checks, all read from index_status: |
| 28 | + 1. No file reports a whole-file parse failure (parse_unusable). |
| 29 | + 2. No range list was clipped without saying so (a trailing "+<N>" marker). |
| 30 | + 3. No single range covers more than 25% of a file of 200 lines or more. |
| 31 | + 4. The flagged-file count stays at or below the checked-in ceiling. |
| 32 | +
|
| 33 | +Data files, both beside this script: |
| 34 | + coverage-gate-allowlist.txt paths the checks skip, each with a written |
| 35 | + reason above it. |
| 36 | + parse-partial-baseline.txt the ceiling for check 4. |
| 37 | +
|
| 38 | +Environment: |
| 39 | + MAX_SINGLE_RANGE_PCT Share limit for check 3 (default 25). |
| 40 | + MIN_FILE_LINES Files below this are exempt from check 3 (default 200). |
| 41 | +
|
| 42 | +Exit 0 when every check passes, 1 when any fails, 2 on a bad argument. |
| 43 | +
|
| 44 | +Options: |
| 45 | + -h, --help This text. |
| 46 | +EOF |
| 47 | +} |
| 48 | + |
| 49 | +BIN="" |
| 50 | +while [ $# -gt 0 ]; do |
| 51 | + case "$1" in |
| 52 | + -h | --help) usage; exit 0 ;; |
| 53 | + -*) echo "self-index-coverage-gate: unknown argument '$1'. Please consult --help." >&2; exit 2 ;; |
| 54 | + *) |
| 55 | + [ -z "$BIN" ] || { |
| 56 | + echo "self-index-coverage-gate: one binary path only. Please consult --help." >&2 |
| 57 | + exit 2 |
| 58 | + } |
| 59 | + BIN="$1" |
| 60 | + ;; |
| 61 | + esac |
| 62 | + shift |
| 63 | +done |
| 64 | +[ -n "$BIN" ] || { echo "self-index-coverage-gate: need a binary path. Please consult --help." >&2; exit 2; } |
| 65 | + |
21 | 66 | REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" |
22 | 67 | ALLOWLIST="${REPO_ROOT}/scripts/ci/coverage-gate-allowlist.txt" |
23 | 68 | BASELINE_FILE="${REPO_ROOT}/scripts/ci/parse-partial-baseline.txt" |
@@ -62,17 +107,32 @@ ALLOWED="${WORK}/allowed.txt" |
62 | 107 | FAILURES=0 |
63 | 108 | note_failure() { echo "FAIL: $*"; FAILURES=$((FAILURES + 1)); } |
64 | 109 |
|
| 110 | +# ── 0. The report's own file list must be complete ──────────────────────── |
| 111 | +# index_status lists at most 500 files per class and sets "truncated" when it |
| 112 | +# drops the rest. Checks 2 and 3 below read that list file by file, so a |
| 113 | +# clipped list means they judge only part of the repo and still print PASS. |
| 114 | +# That is the same silent clipping this gate exists to catch, so stop instead. |
| 115 | +for cls in parse_partial parse_unusable; do |
| 116 | + clipped="$(jq -r --arg c "$cls" '.structuredContent[$c].truncated // false' "${WORK}/status.json")" |
| 117 | + if [ "$clipped" = "true" ]; then |
| 118 | + note_failure "index_status clipped its ${cls} file list — the checks below would see only part of it" |
| 119 | + fi |
| 120 | +done |
| 121 | + |
65 | 122 | # ── 1. Nothing may fail across a whole file ──────────────────────────────── |
66 | 123 | # parse_unusable means one range covers 80%+ of the file, so the report tells |
67 | 124 | # 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 | 125 | UNUSABLE_LISTED="$(jq -r '[.structuredContent.parse_unusable.files[]?.path]|join(" ")' \ |
70 | 126 | "${WORK}/status.json")" |
| 127 | +UNUSABLE=0 |
| 128 | +UNUSABLE_KEPT="" |
71 | 129 | for p in $UNUSABLE_LISTED; do |
72 | | - grep -qxF "$p" "$ALLOWED" && UNUSABLE=$((UNUSABLE - 1)) |
| 130 | + grep -qxF "$p" "$ALLOWED" && continue |
| 131 | + UNUSABLE=$((UNUSABLE + 1)) |
| 132 | + UNUSABLE_KEPT="${UNUSABLE_KEPT}${p} " |
73 | 133 | done |
74 | 134 | if [ "$UNUSABLE" -gt 0 ]; then |
75 | | - note_failure "${UNUSABLE} file(s) report a whole-file parse failure: ${UNUSABLE_LISTED}" |
| 135 | + note_failure "${UNUSABLE} file(s) report a whole-file parse failure: ${UNUSABLE_KEPT}" |
76 | 136 | fi |
77 | 137 |
|
78 | 138 | # ── 2. No range list may be silently clipped ────────────────────────────── |
@@ -104,7 +164,12 @@ done < <(jq -r '.structuredContent.parse_partial.files[]? |
104 | 164 | | "\(.path)\t\(.error_ranges // "")"' "${WORK}/status.json") |
105 | 165 |
|
106 | 166 | # ── 4. The flagged-file count must not drift upward unnoticed ───────────── |
107 | | -CEILING="$(sed -e 's/#.*//' "$BASELINE_FILE" | grep -oE '[0-9]+' | head -1)" |
| 167 | +# One awk, not `grep | head -1`: under `set -o pipefail` head closes the pipe |
| 168 | +# early, grep dies of SIGPIPE, and the whole gate aborts with no message. It |
| 169 | +# does not bite on today's short file, and it would bite the day someone adds |
| 170 | +# a few comment lines. |
| 171 | +CEILING="$(awk '{ sub(/#.*/, "") } match($0, /[0-9]+/) { print substr($0, RSTART, RLENGTH); exit }' \ |
| 172 | + "$BASELINE_FILE")" |
108 | 173 | PARTIAL="$(jq -r '.structuredContent.parse_partial.count // 0' "${WORK}/status.json")" |
109 | 174 | if [ "$PARTIAL" -gt "$CEILING" ]; then |
110 | 175 | note_failure "parse_partial_count is ${PARTIAL}, above the ceiling ${CEILING} in $(basename "$BASELINE_FILE")" |
|
0 commit comments