Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion .github/workflows/pr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ jobs:
# The paginated files endpoint remains filename-only for this gate.
FILES=$(gh api --paginate "repos/$REPO/pulls/$PR/files?per_page=100" --jq '.[].filename')
printf '%s\n' "$FILES"
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
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
echo "product=true" >> "$GITHUB_OUTPUT"
else
echo "product=false" >> "$GITHUB_OUTPUT"
Expand Down Expand Up @@ -127,6 +127,15 @@ jobs:
CCACHE_DIR: ${{ github.workspace }}/.ccache
CCACHE_MAXSIZE: 1000M

# Index this repo with the binary just built and check its own
# parse-coverage report is still useful advice (#963). Ubuntu only: the
# flagged line ranges depend on which conditional-compilation branches
# the preprocessor keeps, so they differ per platform. The gate asserts
# proportions, never exact line numbers.
- name: Parse-coverage gate (Ubuntu)
if: matrix.os == 'ubuntu-latest'
run: scripts/ci/self-index-coverage-gate.sh "$(pwd)/build/c/codebase-memory-mcp"

- name: Build prod + smoke (macOS)
if: matrix.os == 'macos-14'
run: |
Expand Down
4 changes: 3 additions & 1 deletion Makefile.cbm
Original file line number Diff line number Diff line change
Expand Up @@ -98,9 +98,10 @@ endif
KOTLIN_DEDUP_TEST_DEFINE = -DCBM_KOTLIN_DEDUP_TEST_API=1
CALL_REFERENCE_LOOKUP_TEST_DEFINE = -DCBM_CALL_REFERENCE_LOOKUP_TEST_API=1
INCREMENTAL_TEST_DEFINE = -DCBM_INCREMENTAL_TEST_API=1
COVERAGE_MARKER_TEST_DEFINE = -DCBM_COVERAGE_MARKER_TEST_API=1
CFLAGS_TEST = $(CFLAGS_COMMON) $(EDITOR_TEST_DEFINES) $(SANITIZED_DEFINE) \
$(KOTLIN_DEDUP_TEST_DEFINE) $(CALL_REFERENCE_LOOKUP_TEST_DEFINE) \
$(INCREMENTAL_TEST_DEFINE) -g -O1 $(SANITIZE)
$(INCREMENTAL_TEST_DEFINE) $(COVERAGE_MARKER_TEST_DEFINE) -g -O1 $(SANITIZE)
CXXFLAGS_TEST = $(CXXFLAGS_COMMON) $(SANITIZED_DEFINE) -g -O1 $(SANITIZE) $(CXX_STDLIB_FLAGS)

# TSan (can't combine with ASan)
Expand All @@ -118,6 +119,7 @@ TSAN_SANITIZE = -fsanitize=thread -fno-omit-frame-pointer
# macro of ours.
CFLAGS_TSAN = $(CFLAGS_COMMON) $(EDITOR_TEST_DEFINES) $(KOTLIN_DEDUP_TEST_DEFINE) \
$(CALL_REFERENCE_LOOKUP_TEST_DEFINE) $(INCREMENTAL_TEST_DEFINE) \
$(COVERAGE_MARKER_TEST_DEFINE) \
-DCBM_SANITIZED_BUILD=1 -g -O1 $(TSAN_SANITIZE)
CXXFLAGS_TSAN = $(CXXFLAGS_COMMON) -DCBM_SANITIZED_BUILD=1 -g -O1 \
$(TSAN_SANITIZE)
Expand Down
352 changes: 340 additions & 12 deletions internal/cbm/cbm.c

Large diffs are not rendered by default.

15 changes: 15 additions & 0 deletions internal/cbm/cbm.h
Original file line number Diff line number Diff line change
Expand Up @@ -511,6 +511,21 @@ typedef struct CBMFileResult {
* completeness guarantee. Callers should treat a flagged file as "prefer
* grep here", never treat an unflagged file as provably complete. */
bool parse_incomplete;
/* True when the ranges cover so much of the file that they are no longer
* useful advice — one range over 80% of the line count. The file WAS
* indexed, but pointing a reader at almost every line tells them nothing,
* so the report says "read the source" instead of listing the range.
*
* Its main customers are non-C languages. The refinement that narrows a
* whole-file range using the preprocessed parse only runs for C, C++ and
* CUDA, so a Python, Java or Ruby file whose root node is ERROR still
* reports 1-N.
*
* Note the naming: this field and the phase string it produces are both
* `parse_unusable`. The older `parse_incomplete` field emits the phase
* `parse_partial` instead. That mismatch is historical, not deliberate —
* do not copy it. */
bool parse_unusable;
const char *error_ranges;
int error_region_count;
bool is_test_file;
Expand Down
15 changes: 15 additions & 0 deletions scripts/ci/coverage-gate-allowlist.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# Files the self-index coverage gate skips, one repo-relative path per line.
#
# Adding a line here is a deliberate decision, not a convenience. It says:
# "we know this file reports a wide parse-coverage range, we have looked at
# why, and we accept it." Write the reason above the path. A line with no
# reason should be removed rather than trusted.
#
# Blank lines and lines starting with # are ignored.

# One range covers lines 245-327 of a 326-line file (25.5%). The tree-sitter
# PowerShell grammar cannot parse the `} else {` branch that runs to the end
# of the file, so those 83 lines really are absent from the graph. This is a
# genuine grammar gap, not a reporting error. Every other file of 200+ lines
# in this repo sits at 3.9% or below.
scripts/setup-windows.ps1
7 changes: 7 additions & 0 deletions scripts/ci/parse-partial-baseline.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# Ceiling for parse_partial_count when this repo indexes itself.
#
# The number below is what the gate allows. It complements the FLOOR asserted
# in tests/test_index_resilience.c, which stops the signal being switched off
# by accident. Raising this number is allowed but should be explained in the
# commit that does it.
58
118 changes: 118 additions & 0 deletions scripts/ci/self-index-coverage-gate.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
#!/usr/bin/env bash
# Regression guard: this repo's own parse-coverage report must stay useful.
#
# A coverage range is advice — "these lines are missing from the graph, read
# them". Advice stops being advice when it names most of the file, and it
# stops being honest when the list was clipped without saying so. Both things
# happened here before (#963): src/cli/cli.c reported its whole 13,046 lines
# as one range, and two caps in series dropped ranges with no signal at all.
#
# This indexes the repo with a given binary and fails if any of that comes back.
#
# Usage: self-index-coverage-gate.sh <path-to-codebase-memory-mcp-binary>
#
# NOTE ON PLATFORM: the ranges depend on which conditional-compilation branches
# the preprocessor keeps. On a machine where _WIN32 is defined the discarded
# branches swap and a different set of lines is flagged. That is why this runs
# on ONE CI leg and asserts proportions rather than exact line numbers.
set -euo pipefail

BIN="${1:?usage: self-index-coverage-gate.sh <path-to-binary>}"
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
ALLOWLIST="${REPO_ROOT}/scripts/ci/coverage-gate-allowlist.txt"
BASELINE_FILE="${REPO_ROOT}/scripts/ci/parse-partial-baseline.txt"

# Share of a file one range may cover before it stops being useful advice.
# The worst real offender today is src/cli/cli.c at 3.9%, so this has room.
MAX_SINGLE_RANGE_PCT="${MAX_SINGLE_RANGE_PCT:-25}"
# Files below this are exempt: a 5-line fixture with a 3-line range is 60% of
# itself and says nothing about report quality.
MIN_FILE_LINES="${MIN_FILE_LINES:-200}"

command -v jq >/dev/null || { echo "FAIL: jq is required"; exit 1; }

WORK="$(mktemp -d)"
# The runtime dir holds a unix socket, and a socket path has a hard length
# limit (~104 bytes). macOS puts mktemp under /var/folders/<long>/T/, which
# blows that limit and fails with "secure CLI coordination could not be
# created (endpoint)". Keep the runtime dir short and separate from the cache.
RUNTIME="/tmp/cbm-gate.$$"
trap 'rm -rf "$WORK" "$RUNTIME"' EXIT
export CBM_CACHE_DIR="${WORK}/cache"
export CBM_RUNTIME_DIR="$RUNTIME"
mkdir -p "$CBM_CACHE_DIR" "$RUNTIME"

echo "==> indexing ${REPO_ROOT} with $(basename "$BIN")"
"$BIN" cli index_repository --repo-path "$REPO_ROOT" --mode full --json \
> "${WORK}/index.json" 2>"${WORK}/index.err" || {
echo "FAIL: index_repository exited non-zero"; tail -20 "${WORK}/index.err"; exit 1; }

PROJECT="$(jq -r '.structuredContent.project // empty' "${WORK}/index.json")"
[ -n "$PROJECT" ] || { echo "FAIL: index_repository did not name a project"; exit 1; }

"$BIN" cli index_status --project "$PROJECT" --json > "${WORK}/status.json" 2>/dev/null || {
echo "FAIL: index_status exited non-zero"; exit 1; }

# Allowlisted paths, comments and blanks stripped.
ALLOWED="${WORK}/allowed.txt"
: > "$ALLOWED"
[ -f "$ALLOWLIST" ] && sed -e 's/#.*//' -e 's/[[:space:]]*$//' "$ALLOWLIST" \
| grep -v '^$' > "$ALLOWED" || true

FAILURES=0
note_failure() { echo "FAIL: $*"; FAILURES=$((FAILURES + 1)); }

# ── 1. Nothing may fail across a whole file ────────────────────────────────
# parse_unusable means one range covers 80%+ of the file, so the report tells
# a reader to go read the source. Zero today; a new one is a real regression.
UNUSABLE="$(jq -r '.structuredContent.parse_unusable.count // 0' "${WORK}/status.json")"
UNUSABLE_LISTED="$(jq -r '[.structuredContent.parse_unusable.files[]?.path]|join(" ")' \
"${WORK}/status.json")"
for p in $UNUSABLE_LISTED; do
grep -qxF "$p" "$ALLOWED" && UNUSABLE=$((UNUSABLE - 1))
done
if [ "$UNUSABLE" -gt 0 ]; then
note_failure "${UNUSABLE} file(s) report a whole-file parse failure: ${UNUSABLE_LISTED}"
fi

# ── 2. No range list may be silently clipped ──────────────────────────────
# A trailing "+<N>" says the producer's cap threw N ranges away. With the cap
# at 256 a file that still overflows is worth stopping for.
TRUNCATED="$(jq -r '[.structuredContent.parse_partial.files[]?
| select(.error_ranges? // "" | test("\\+[0-9]+$")) | .path] | join(" ")' \
"${WORK}/status.json")"
for p in $TRUNCATED; do
grep -qxF "$p" "$ALLOWED" && continue
note_failure "$p carries a +N truncation marker — its range list was clipped"
done

# ── 3. No single range may cover a quarter of its file ────────────────────
while IFS=$'\t' read -r path ranges; do
[ -n "$path" ] || continue
grep -qxF "$path" "$ALLOWED" && continue
[ -f "${REPO_ROOT}/${path}" ] || continue
total="$(wc -l < "${REPO_ROOT}/${path}" | tr -d ' ')"
[ "$total" -ge "$MIN_FILE_LINES" ] || continue
widest="$(printf '%s' "$ranges" | tr ',' '\n' | grep '^[0-9]' \
| awk -F- '{d=$2-$1+1; if (d>m) m=d} END {print m+0}')"
pct="$(awk -v a="$widest" -v b="$total" 'BEGIN{printf "%.1f", 100*a/b}')"
over="$(awk -v p="$pct" -v lim="$MAX_SINGLE_RANGE_PCT" 'BEGIN{print (p>lim)?1:0}')"
if [ "$over" = "1" ]; then
note_failure "$path has one range of ${widest} lines — ${pct}% of ${total}, over ${MAX_SINGLE_RANGE_PCT}%"
fi
done < <(jq -r '.structuredContent.parse_partial.files[]?
| "\(.path)\t\(.error_ranges // "")"' "${WORK}/status.json")

# ── 4. The flagged-file count must not drift upward unnoticed ─────────────
CEILING="$(sed -e 's/#.*//' "$BASELINE_FILE" | grep -oE '[0-9]+' | head -1)"
PARTIAL="$(jq -r '.structuredContent.parse_partial.count // 0' "${WORK}/status.json")"
if [ "$PARTIAL" -gt "$CEILING" ]; then
note_failure "parse_partial_count is ${PARTIAL}, above the ceiling ${CEILING} in $(basename "$BASELINE_FILE")"
fi

echo "==> parse_partial=${PARTIAL} (ceiling ${CEILING}) parse_unusable=${UNUSABLE} allowlisted=$(wc -l < "$ALLOWED" | tr -d ' ')"
if [ "$FAILURES" -gt 0 ]; then
echo "FAIL: ${FAILURES} coverage-gate check(s) failed"
exit 1
fi
echo "PASS: parse-coverage report is within bounds"
Loading
Loading