diff options
| author | Craig Jennings <c@cjennings.net> | 2026-05-31 11:43:03 -0500 |
|---|---|---|
| committer | Craig Jennings <c@cjennings.net> | 2026-05-31 11:43:03 -0500 |
| commit | b46619cd17ed4e36f2e59c1b600078521b2049ef (patch) | |
| tree | f128aeef3f0f679a400595c896a98618266706d9 /scripts | |
| parent | 3640664e0fa11d7eb99c2900df57734b411e2d2b (diff) | |
| download | rulesets-b46619cd17ed4e36f2e59c1b600078521b2049ef.tar.gz rulesets-b46619cd17ed4e36f2e59c1b600078521b2049ef.zip | |
feat(elisp): add coverage-summary to the Elisp bundle with missing-file detection
A line-weighted coverage total has a blind spot: a module no test loads never shows up in the SimpleCov report, so it can't drag the number down. The suite looks healthier than it is. This adds a summary that counts every source file on disk against the report and treats an absent file as 0%, weighting the project number by file instead of by line so untested modules stay visible.
The script ships at languages/elisp/claude/scripts/coverage-summary.el, self-contained on stock Emacs (just the built-in json). It parses the undercover SimpleCov shape directly rather than depending on the editor's coverage engine, so it runs anywhere the bundle lands. I proved it against a real 103-file report: 93 tracked, 27 untested modules surfaced, project number 66.4%.
Delivery follows the bundle convention. The script lives under the gitignored .claude/ footprint and gets auto-fixed on drift by sync-language-bundle.sh, which I made generic for any claude/scripts/* rather than coverage-specific. The Makefile targets ship as a project-owned fragment (languages/elisp/coverage-makefile.txt) that install-lang.sh seeds at the project root and sync drops into .ai/inbox/ when that convention exists. The bundle never edits the project's own Makefile.
Tests: 12 ERT for the kernel (Normal/Boundary/Error per function), wired into make test via a new languages/*/tests/ discovery path, plus bats for the sync auto-fix and the inbox-drop guards.
This is the Elisp pilot. The pattern is proven, so fanning out to Python, Go, and TypeScript is now a follow-up. Each one needs only its own parser and fragment. The plumbing is already generic.
Diffstat (limited to 'scripts')
| -rwxr-xr-x | scripts/install-lang.sh | 11 | ||||
| -rwxr-xr-x | scripts/sync-language-bundle.sh | 26 | ||||
| -rw-r--r-- | scripts/tests/install-lang.bats | 17 | ||||
| -rw-r--r-- | scripts/tests/sync-language-bundle.bats | 62 |
4 files changed, 116 insertions, 0 deletions
diff --git a/scripts/install-lang.sh b/scripts/install-lang.sh index 4097bde..0fc9ea8 100755 --- a/scripts/install-lang.sh +++ b/scripts/install-lang.sh @@ -76,6 +76,17 @@ if [ -f "$SRC/CLAUDE.md" ]; then fi fi +# 3b. coverage-makefile.txt — project-owned Makefile fragment, seed on first +# install. Never overwrites (the project edits its own copy) unless FORCE=1. +if [ -f "$SRC/coverage-makefile.txt" ]; then + if [ -f "$PROJECT/coverage-makefile.txt" ] && [ "$FORCE" != "1" ]; then + echo " [skip] coverage-makefile.txt already exists (use FORCE=1 to overwrite)" + else + cp "$SRC/coverage-makefile.txt" "$PROJECT/coverage-makefile.txt" + echo " [ok] coverage-makefile.txt installed (copy its targets into your Makefile)" + fi +fi + # 4. .gitignore — append missing lines (deduped, skip comments) if [ -f "$SRC/gitignore-add.txt" ]; then touch "$PROJECT/.gitignore" diff --git a/scripts/sync-language-bundle.sh b/scripts/sync-language-bundle.sh index b2db8cb..25af11b 100755 --- a/scripts/sync-language-bundle.sh +++ b/scripts/sync-language-bundle.sh @@ -84,6 +84,22 @@ surface() { MANUAL=$((MANUAL + 1)) } +# inbox_drop SRC BASENAME — offer a project-owned file via the .ai/ inbox. +# Only acts when the project uses the .ai/ inbox convention (so a bundle target +# without .ai/ is never given an empty one). No-op once the project has already +# adopted the file at its root or has a copy waiting in the inbox. +inbox_drop() { + local src="$1" base="$2" inbox="$PROJECT/.ai/inbox" + [ -f "$src" ] || return 0 + [ -d "$inbox" ] || return 0 + [ -f "$PROJECT/$base" ] && return 0 # already adopted at project root + ls "$inbox"/*"$base" >/dev/null 2>&1 && return 0 # already waiting in inbox + cp "$src" "$inbox/from-rulesets-$base" + ensure_header + OUT+=" inbox .ai/inbox/from-rulesets-$base (project-owned — adopt deliberately)"$'\n' + FIXED=$((FIXED + 1)) +} + # process_bundle KIND SRC_DIR — reconcile one bundle (KIND is language|team). # A team overlay owns only its own rule files; a language bundle also owns the # shared generic rules, its hooks/githooks, and surfaces settings.json. @@ -131,6 +147,12 @@ process_bundle() { fix "$f" "$PROJECT/.claude/$rel" exec done < <(find "$src/claude/hooks" -type f) fi + if [ -d "$src/claude/scripts" ]; then + while IFS= read -r f; do + rel="${f#"$src"/claude/}" + fix "$f" "$PROJECT/.claude/$rel" + done < <(find "$src/claude/scripts" -type f) + fi if [ -d "$src/githooks" ]; then while IFS= read -r f; do rel="${f#"$src"/githooks/}" @@ -138,6 +160,10 @@ process_bundle() { done < <(find "$src/githooks" -type f) fi surface "$src/claude/settings.json" "$PROJECT/.claude/settings.json" + # The Makefile fragment is project-owned: never auto-fix it, never edit the + # project Makefile. If the project uses the .ai/ inbox convention and hasn't + # already adopted the fragment, drop a copy there for deliberate adoption. + inbox_drop "$src/coverage-makefile.txt" "coverage-makefile.txt" fi if [ "$MANUAL" -gt "$manual_before" ]; then diff --git a/scripts/tests/install-lang.bats b/scripts/tests/install-lang.bats index 523be99..a26c3d5 100644 --- a/scripts/tests/install-lang.bats +++ b/scripts/tests/install-lang.bats @@ -25,6 +25,23 @@ teardown() { [ -d "$PROJECT/.claude/rules" ] [ -d "$PROJECT/githooks" ] [ -f "$PROJECT/CLAUDE.md" ] + # The coverage-summary script ships inside the gitignored .claude footprint. + [ -f "$PROJECT/.claude/scripts/coverage-summary.el" ] +} + +@test "install-lang elisp: seeds the project-owned coverage Makefile fragment" { + run bash "$INSTALL_LANG" elisp "$PROJECT" + + [ "$status" -eq 0 ] + [ -f "$PROJECT/coverage-makefile.txt" ] +} + +@test "install-lang elisp: does not overwrite an existing fragment without FORCE" { + echo "MY OWN VERSION" > "$PROJECT/coverage-makefile.txt" + run bash "$INSTALL_LANG" elisp "$PROJECT" + + [ "$status" -eq 0 ] + grep -qxF "MY OWN VERSION" "$PROJECT/coverage-makefile.txt" } @test "install-lang elisp: gitignores the full Claude tooling footprint" { diff --git a/scripts/tests/sync-language-bundle.bats b/scripts/tests/sync-language-bundle.bats index e641646..5e3b912 100644 --- a/scripts/tests/sync-language-bundle.bats +++ b/scripts/tests/sync-language-bundle.bats @@ -29,6 +29,10 @@ install_bundle() { mkdir -p "$proj/.claude/hooks" cp -r "$REAL_REPO/languages/$lang/claude/hooks/." "$proj/.claude/hooks/" fi + if [ -d "$REAL_REPO/languages/$lang/claude/scripts" ]; then + mkdir -p "$proj/.claude/scripts" + cp -r "$REAL_REPO/languages/$lang/claude/scripts/." "$proj/.claude/scripts/" + fi if [ -f "$REAL_REPO/languages/$lang/claude/settings.json" ]; then cp "$REAL_REPO/languages/$lang/claude/settings.json" "$proj/.claude/settings.json" fi @@ -120,6 +124,64 @@ install_team_overlay() { [ -x "$PROJ/.claude/hooks/validate-el.sh" ] } +# --- Auto-fix: .claude/scripts --- + +@test "sync: drifted bundle script is auto-fixed and restored" { + install_bundle elisp "$PROJ" + echo ";; junk drift" >> "$PROJ/.claude/scripts/coverage-summary.el" + run bash "$SCRIPT" "$PROJ" + [ "$status" -eq 0 ] + [[ "$output" == *".claude/scripts/coverage-summary.el"* ]] + matches_canonical ".claude/scripts/coverage-summary.el" "$REAL_REPO/languages/elisp/claude/scripts/coverage-summary.el" +} + +@test "sync: missing bundle script is re-copied" { + install_bundle elisp "$PROJ" + rm "$PROJ/.claude/scripts/coverage-summary.el" + run bash "$SCRIPT" "$PROJ" + [ "$status" -eq 0 ] + [[ "$output" == *".claude/scripts/coverage-summary.el"* ]] + [ -f "$PROJ/.claude/scripts/coverage-summary.el" ] +} + +# --- Project-owned: Makefile fragment via inbox --- + +@test "sync: coverage Makefile fragment is dropped into .ai/inbox when present" { + install_bundle elisp "$PROJ" + mkdir -p "$PROJ/.ai/inbox" + run bash "$SCRIPT" "$PROJ" + [ "$status" -eq 0 ] + [[ "$output" == *"inbox"* ]] + [ -f "$PROJ/.ai/inbox/from-rulesets-coverage-makefile.txt" ] + matches_canonical ".ai/inbox/from-rulesets-coverage-makefile.txt" "$REAL_REPO/languages/elisp/coverage-makefile.txt" +} + +@test "sync: no .ai/inbox means no fragment drop and no empty .ai/ created" { + install_bundle elisp "$PROJ" + run bash "$SCRIPT" "$PROJ" + [ "$status" -eq 0 ] + [[ "$output" != *"inbox"* ]] + [ ! -d "$PROJ/.ai" ] +} + +@test "sync: fragment already adopted at project root is not re-dropped" { + install_bundle elisp "$PROJ" + mkdir -p "$PROJ/.ai/inbox" + cp "$REAL_REPO/languages/elisp/coverage-makefile.txt" "$PROJ/coverage-makefile.txt" + run bash "$SCRIPT" "$PROJ" + [ "$status" -eq 0 ] + [ ! -f "$PROJ/.ai/inbox/from-rulesets-coverage-makefile.txt" ] +} + +@test "sync: fragment already waiting in inbox is not duplicated" { + install_bundle elisp "$PROJ" + mkdir -p "$PROJ/.ai/inbox" + cp "$REAL_REPO/languages/elisp/coverage-makefile.txt" "$PROJ/.ai/inbox/from-rulesets-coverage-makefile.txt" + run bash "$SCRIPT" "$PROJ" + [ "$status" -eq 0 ] + [[ "$output" != *"inbox"* ]] +} + # --- Surface-only: settings.json --- @test "sync: drifted settings.json is surfaced, NOT modified, exit 3" { |
