cc113c732f
- 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>
233 lines
11 KiB
Bash
Executable File
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 ]
|