Files
improvise/scripts/repo-stats-test.sh
T
Edward Langley cc113c732f docs(context): answer the review of the repo-map split
- repo-stats reads the gotcha list from context/gotchas.md (the digest in
  repo-map.md no longer measures the doc-growth signal) and reports it as
  `docs.gotchas`; the test fixture and its derived expectations follow.
- AGENTS.md and the wave agent-rules protect gotchas.md and
  file-inventory.md like the repo map they were split from.
- doc-site.md's publication table lists the two new pages.
- file-inventory.md's Context docs rows describe the three files correctly.
- the code-review skill points at gotchas.md for the long form.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-09 21:42:24 -07:00

233 lines
11 KiB
Bash
Executable File

#!/usr/bin/env bash
# check_the_checker for scripts/repo-stats (improvise-1xfb.5.1): the whole pipeline,
# code-maat and improvise included, over a fixture repo whose every number is known.
# See context/test-evidence/tooling.md.
set -uo pipefail
here="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
STATS="$here/repo-stats"
work="$(mktemp -d)"
trap 'rm -rf "$work"' EXIT
pass=0; fail=0
ok() { printf ' ok %s\n' "$1"; pass=$((pass+1)); }
bad() { printf ' FAIL %s (%s)\n' "$1" "$2"; fail=$((fail+1)); }
# The improvise under test: on PATH in CI, else this tree's debug build. No build
# here: checkers.sh scrubs the linker's environment (context/code/tooling.md §7).
if command -v improvise >/dev/null; then
IMPROVISE="$(command -v improvise)"
elif [ -x "$here/../target/debug/improvise" ]; then
IMPROVISE="$here/../target/debug/improvise"
else
echo "repo-stats-test: no improvise; run \`cargo build --bin improvise\` first" >&2
exit 1
fi
export IMPROVISE
# $1 name, $2 jq expression over $work/out.json, $3 the value it must print
is() {
local got
got="$(jq -c "$2" "$work/out.json" 2>&1)"
[ "$got" = "$3" ] && ok "$1" || bad "$1" "got $got, wanted $3"
}
# $1 name, $2 table, $3 row label, $4 the row's cells after the label, as JSON
row() {
is "$1" ".tables[\"$2\"][]|select(.[0]==\"$3\")|.[1:]" "$4"
}
# $1 name, $2 fixed-string the text report must contain
says() {
grep -Fq -- "$2" "$work/out.txt" && ok "$1" || bad "$1" "no '$2' in the text report"
}
export GIT_CONFIG_NOSYSTEM=1 GIT_CONFIG_GLOBAL=/dev/null
export GIT_AUTHOR_NAME=fixture GIT_AUTHOR_EMAIL=f@x GIT_COMMITTER_NAME=fixture GIT_COMMITTER_EMAIL=f@x
repo="$work/repo"
g() { git -C "$repo" "$@" >/dev/null; }
# $1 path, $2 line count, $3 tag. Rewriting with a new tag replaces every line.
put() { mkdir -p "$(dirname "$repo/$1")"; seq -f "$3 %g" 1 "$2" > "$repo/$1"; }
# $1 date; commits everything staged or not.
commit() {
g add -A
GIT_AUTHOR_DATE="$1T12:00:00Z" GIT_COMMITTER_DATE="$1T12:00:00Z" g commit -q -m "at $1"
}
git init -q -b main "$repo"
# Before the window: must count nowhere.
put README.md 50 old; commit 2026-01-01
# Two commits co-change six files, so each pair couples at 100% over 2 revs.
# src/ is the root crate's, so it crosses a boundary with both crates/.
for f in crates/a/src/lib.rs crates/b/src/lib.rs; do put "$f" 10 x1; done
put src/main.rs 5 x1; put crates/a/tests/t.rs 4 x1
put AGENTS.md 3 x1; put .agents/skills/wave/agent-rules.md 3 x1
commit 2026-06-02
for f in crates/a/src/lib.rs crates/b/src/lib.rs; do put "$f" 6 x2; done
put src/main.rs 5 x2; put crates/a/tests/t.rs 4 x2
put AGENTS.md 3 x2; put .agents/skills/wave/agent-rules.md 3 x2
commit 2026-06-03
# One file in each remaining area. The repo map is 13 lines.
put context/code/x.md 8 x3
mkdir -p "$repo/context/test-evidence"
printf '### t_one\ntext\n#### t_two\n## not an entry\n### t_three\n' \
> "$repo/context/test-evidence/y.md"
printf '# Map\n' > "$repo/context/repo-map.md"
cat > "$repo/context/gotchas.md" <<'EOF'
# Gotchas
1. **One.** short.
2. **Two.** a much longer gotcha, with plenty of words in it to be the longest one
and a second line.
3. **Three.** middling length.
---
4. not a gotcha, because the section has ended
EOF
put context/plan.md 2 x3
put prolog-sessions/kb.pl 5 x3
put scripts/foo.sh 7 x3; put .github/workflows/ci.yml 1 x3
put web/app.js 9 x3; put harness/x.js 2 x3
put Cargo.lock 11 x3
put crates/a/src/grammar.pest 3 x3
put .beads/issues.jsonl 2 x3; put site/index.html 1 x3
put docs/guide.md 2 x3; put .codex/config.toml 1 x3
commit 2026-06-04
g tag before-shrink
# A side branch merged with a merge commit: the side commit counts, the merge does not.
g checkout -q -b side
put notes.txt 4 x5; commit 2026-06-05
g checkout -q main
GIT_AUTHOR_DATE=2026-06-06T12:00:00Z GIT_COMMITTER_DATE=2026-06-06T12:00:00Z \
g merge -q --no-ff -m merge side
# A shrink in the next ISO week, so one area deletes and one row spans two weeks,
# and a rename, which --no-renames counts as a delete and an add.
put web/app.js 3 x4
g mv scripts/foo.sh scripts/bar.sh
commit 2026-06-09
# $1 output dir, $2 "text" to also write the text report, rest: extra args.
run() {
local out="$1" text="$2"; shift 2
"$STATS" -C "$repo" --since 2026-06-01 --min-revs 2 --out "$out" "$@" --json \
> "$work/out.json" 2> "$work/err" \
|| { bad "repo-stats $* ran" "$(cat "$work/err")"; return 1; }
[ "$text" = text ] || return 0
"$STATS" -C "$repo" --since 2026-06-01 --min-revs 2 --out "$out" "$@" \
> "$work/out.txt" 2>&1 \
|| { bad "repo-stats $* ran (text)" "$(cat "$work/out.txt")"; return 1; }
}
if run "$work/stats" text; then
is "no gh/main in the fixture, so the ref is HEAD" '.ref' '"HEAD"'
is "the merge commit is not counted, the side commit is" '.commits' '5'
row "rust src is .rs and .pest outside tests" by-area "rust src" '["45","25","0.556"]'
row "rust tests are the tests/ files" by-area "rust tests" '["8","4","0.500"]'
row "agent docs and skills take .codex" by-area "agent docs and skills" '["13","6","0.462"]'
row "docs/*.md is other docs" by-area "other docs" '["2","0","0.000"]'
row "context/code" by-area "context/code" '["8","0","0.000"]'
row "context/test-evidence" by-area "context/test-evidence" '["5","0","0.000"]'
row "repo-map is its own area" by-area "repo-map" '["1","0","0.000"]'
row "other context" by-area "other context" '["13","0","0.000"]'
row "kb.pl" by-area "kb.pl" '["5","0","0.000"]'
row "scripts/CI takes .github, and a rename is a delete and an add" \
by-area "scripts/CI" '["15","7","0.467"]'
row "web and harness, and its deletion ratio" by-area "web and harness" '["14","9","0.643"]'
row "a lock file, .beads/ and site/ are generated" by-area "generated" '["14","0","0.000"]'
row "the pre-window README counts nowhere" by-area "other" '["4","0","0.000"]'
row "prose is the context areas, other docs and agent docs" by-kind prose '["42","6","0.143"]'
row "code is rust, scripts/CI and web" by-kind code '["82","45","0.549"]'
row "prose to code is prose over code" by-kind ProseToCode '["0.512","0.133",""]'
is "weeks are columns, first-seen first" '.tables["area-by-week"][0][1:]' '["2026-W24","2026-W23"]'
row "an area is split by ISO week" area-by-week "web and harness" '["3","11"]'
is "every day's churn.csv total equals code-maat's abs-churn" \
'[.tables["by-day"][1:][]|.[1]==.[2]]|all' 'true'
row "a day's total is every file changed that day" by-day 2026-06-04 '["71","71"]'
is "gotchas end with their section" '.docs.gotchas.count' '3'
is "the longest gotcha is measured over its lines" \
'.docs.gotchas.longest|[.number,.lines]' '[2,2]'
is "test-evidence entries are ### and #### headings" '.docs.test_evidence.entries' '3'
is "coupling finds the 15 pairs of six files, and web/app.js with scripts/foo.sh" \
'.coupling|length' '16'
is "files of two crates, the root crate's included, cross a boundary" \
'[.coupling[]|select(.["crosses-crate"]=="yes")|[.entity,.coupled]|sort]|sort' \
'[["crates/a/src/lib.rs","crates/b/src/lib.rs"],["crates/a/src/lib.rs","src/main.rs"],["crates/a/tests/t.rs","crates/b/src/lib.rs"],["crates/a/tests/t.rs","src/main.rs"],["crates/b/src/lib.rs","src/main.rs"]]'
is "two agent-rule docs are flagged" \
'[.coupling[]|select(.["agent-rules"]=="yes")|[.entity,.coupled]|sort]' \
'[[".agents/skills/wave/agent-rules.md","AGENTS.md"]]'
says "the text report flags a crate crossing" "[CROSSES-CRATE]"
says "the text report flags agent-rule docs" "[AGENT-RULES]"
says "the text report prints improvise's ratio row" "ProseToCode"
views="$(grep -c '^## View: ' "$work/stats/repo-stats.improv")"
[ "$views" = 10 ] && ok "the model keeps a view per file and the report's four" \
|| bad "the model keeps a view per file and the report's four" "$views views"
# Sparse, not padded: a padded merge has the same numbers and views, but a
# `-` item in every category and per-file views seated on the padding.
dashes="$(grep -c '|-|' "$work/stats/repo-stats.improv")"
[ "$dashes" = 0 ] && ok "the sparse model has no padding item" \
|| bad "the sparse model has no padding item" "$dashes lines name |-|"
churn_view="$(awk '/^## View: churn$/,/^$/' "$work/stats/repo-stats.improv")"
for seat in "date: row" "agent-rules: none" "coupled: none" "crosses-crate: none"; do
grep -qx -- "$seat" <<< "$churn_view" && ok "the churn view seats $seat" \
|| bad "the churn view seats $seat" "$(tr '\n' ' ' <<< "$churn_view")"
done
fi
# An --out holding a CSV this script did not write is refused, since the folder
# import would merge it. The script's own CSVs, from a rerun, are not foreign.
mkdir -p "$work/foreign"; echo "x,y" > "$work/foreign/other.csv"
"$STATS" -C "$repo" --since 2026-06-01 --out "$work/foreign" > /dev/null 2>&1
got=$?
[ "$got" = 2 ] && ok "a foreign CSV in --out is refused with exit 2" \
|| bad "a foreign CSV in --out is refused with exit 2" "exit $got"
# The window is scoped to the ref: at the tag, the shrink has not happened.
if run "$work/at-tag" json --ref before-shrink; then
row "--ref scopes the log" by-area "web and harness" '["11","0","0.000"]'
is "--ref names the ref it read" '.ref' '"before-shrink"'
fi
# A window whose only commits are empty changes no file: exit 0, and no model.
empty="$work/empty"
git init -q -b main "$empty"
repo="$empty"; put README.md 5 old; commit 2026-01-01
GIT_AUTHOR_DATE=2026-06-02T12:00:00Z GIT_COMMITTER_DATE=2026-06-02T12:00:00Z \
g commit -q --allow-empty -m empty
if run "$work/empty-out" text; then
is "an empty commit is counted" '.commits' '1'
is "and builds no tables" '.tables' '{}'
lines="$(wc -l < "$work/empty-out/churn.csv" | tr -d ' ')"
[ "$lines" = 1 ] && ok "churn.csv is written with its header alone" \
|| bad "churn.csv is written with its header alone" "$lines lines"
says "the text report says why there are no tables" "No file changed in the window"
fi
repo="$work/repo"
# improvise refusing the pipeline is exit 3, whatever the numbers were. The
# third stub imports cleanly and refuses only the scripts.
mkdir -p "$work/bin"
printf '#!/usr/bin/env bash\necho "Parse error: no" >&2\nexit 1\n' > "$work/bin/refuses"
printf '#!/usr/bin/env bash\necho "warning: Skipped 3 of 3 records" >&2\nexit 0\n' \
> "$work/bin/warns"
printf '#!/usr/bin/env bash\n[ "$1" = script ] || exit 0\necho "Parse error: no" >&2\nexit 1\n' \
> "$work/bin/refuses-a-script"
chmod +x "$work/bin/refuses" "$work/bin/warns" "$work/bin/refuses-a-script"
for stub in refuses warns refuses-a-script; do
IMPROVISE="$work/bin/$stub" "$STATS" -C "$repo" --since 2026-06-01 --out "$work/$stub" \
> /dev/null 2>&1
got=$?
[ "$got" = 3 ] && ok "an improvise that $stub fails the pipeline with exit 3" \
|| bad "an improvise that $stub fails the pipeline with exit 3" "exit $got"
done
printf '\nrepo-stats: %d passed, %d failed\n' "$pass" "$fail"
[ "$fail" = 0 ]