aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorCraig Jennings <c@cjennings.net>2026-05-31 13:07:40 -0500
committerCraig Jennings <c@cjennings.net>2026-05-31 13:07:40 -0500
commit47ca509e69b6a1472a735a4b9521a952e7434491 (patch)
tree9101eb376d233c455f653ad857474b3a0dae767f
parentaf478a42b18c4d5e0712c4cb43036126d36c56b5 (diff)
downloadrulesets-47ca509e69b6a1472a735a4b9521a952e7434491.tar.gz
rulesets-47ca509e69b6a1472a735a4b9521a952e7434491.zip
feat(go): add coverage-summary as a Go bundle coverage slice
Third language in the coverage-summary fan-out, after Elisp and Python. Same kernel: count every source file on disk that's absent from the coverage profile as 0% and weight the project number by file, so an untested file stays visible instead of being averaged away. The script at languages/go/claude/scripts/coverage-summary.go parses a cover.out profile, maps each import-path-qualified entry back to an on-disk relative path using the module path from go.mod, and reports a file-weighted number plus the missing files. It's standard library only, so it runs anywhere via go run, and it doesn't reimplement the per-function table that go tool cover -func already prints. I proved it against a real go test -coverprofile run, not just a synthetic fixture, since the Go toolchain is installed here. Two findings to flag. Modern go test ./... already lists every module package in the profile at 0% even when untested, so for in-module code the missing-file list is usually empty. The detection earns its keep on build-tagged files and dirs outside ./.... And this is a coverage-only slice of a Go bundle that doesn't otherwise exist yet: there's no go.md rule file, so sync-language-bundle.sh can't fingerprint it (detection keys on a bundle's own .claude/rules). The script installs via make install-lang LANG=go but won't be sync-maintained until the Go bundle gets real rules and a CLAUDE.md. Building that out is the natural companion task. Tests are black-box: a Go test in its own throwaway module runs the script via go run against temp fixtures and checks output, so the shipped script dir stays test-free. They cover missing-file detection, all-tracked, _test.go exclusion, and the missing-report error. make test gained a go test discovery path for languages/*/tests, guarded so environments without Go skip it cleanly.
-rw-r--r--Makefile9
-rw-r--r--languages/go/claude/scripts/coverage-summary.go248
-rw-r--r--languages/go/coverage-makefile.txt43
-rw-r--r--languages/go/gitignore-add.txt9
-rw-r--r--languages/go/tests/coverage_summary_test.go142
-rw-r--r--languages/go/tests/go.mod3
-rw-r--r--scripts/tests/install-lang.bats12
-rw-r--r--todo.org13
8 files changed, 472 insertions, 7 deletions
diff --git a/Makefile b/Makefile
index f9426f4..b8f34cb 100644
--- a/Makefile
+++ b/Makefile
@@ -468,6 +468,15 @@ test: ## Run all test suites (pytest + ERT + bats)
echo "pytest: $$d"; \
( cd "$$d" && python3 -m pytest -q ); \
done
+ @if command -v go >/dev/null 2>&1; then \
+ set -e; for d in languages/*/tests; do \
+ ls "$$d"/*_test.go >/dev/null 2>&1 || continue; \
+ echo "go test: $$d"; \
+ ( cd "$$d" && go test ./... ); \
+ done; \
+ else \
+ echo "go test: skipped (go not installed)"; \
+ fi
@set -e; for f in .ai/scripts/tests/test-*.el; do \
[ -e "$$f" ] || continue; \
echo "ert: $$(basename "$$f")"; \
diff --git a/languages/go/claude/scripts/coverage-summary.go b/languages/go/claude/scripts/coverage-summary.go
new file mode 100644
index 0000000..20bd5e7
--- /dev/null
+++ b/languages/go/claude/scripts/coverage-summary.go
@@ -0,0 +1,248 @@
+// Command coverage-summary prints a whole-project coverage summary from a Go
+// coverage profile (cover.out).
+//
+// Batch helper for `make coverage-summary`. After `go test -coverprofile` writes
+// a profile, this prints a file-weighted project number and the source files
+// present on disk but absent from the profile.
+//
+// The value here is the missing-file detection: a package no test exercises
+// never appears in the profile, so a statement-weighted total silently skips it
+// and the suite looks healthier than it is. This counts such a file as 0% and
+// weights the project number by file rather than by statement, so untested
+// files stay visible. `go tool cover -func` already prints the per-function /
+// per-file table, so this deliberately doesn't reimplement that.
+//
+// Self-contained on the standard library, so it runs anywhere the bundle lands
+// via `go run coverage-summary.go`. The cover.out line shape it parses is:
+//
+// <import-path>/<file>.go:startLine.col,endLine.col numStmts count
+//
+// Profile paths are import-path-qualified; the module path from the project's
+// go.mod maps them back to on-disk relative paths.
+//
+// Usage:
+//
+// go run coverage-summary.go PROFILE SOURCE_DIR [PROJECT_ROOT]
+// PROFILE path to the cover.out coverage profile
+// SOURCE_DIR the directory to account for (walked recursively for *.go)
+// PROJECT_ROOT module root holding go.mod; defaults to the current directory
+package main
+
+import (
+ "bufio"
+ "fmt"
+ "os"
+ "path/filepath"
+ "sort"
+ "strconv"
+ "strings"
+)
+
+type counts struct{ covered, total int }
+
+func die(format string, args ...any) {
+ fmt.Fprintf(os.Stderr, "coverage-summary: "+format+"\n", args...)
+ os.Exit(1)
+}
+
+// readModule returns the module path from PROJECT_ROOT/go.mod, or "" if absent.
+func readModule(root string) string {
+ f, err := os.Open(filepath.Join(root, "go.mod"))
+ if err != nil {
+ return ""
+ }
+ defer f.Close()
+ sc := bufio.NewScanner(f)
+ for sc.Scan() {
+ line := strings.TrimSpace(sc.Text())
+ if rest, ok := strings.CutPrefix(line, "module "); ok {
+ return strings.TrimSpace(rest)
+ }
+ }
+ _ = sc.Err() // a read error just means an unknown module path; paths stay unstripped
+ return ""
+}
+
+// stripModule turns an import-path-qualified file into a module-relative path.
+func stripModule(path, module string) string {
+ if module != "" {
+ if rest, ok := strings.CutPrefix(path, module+"/"); ok {
+ return rest
+ }
+ }
+ return path
+}
+
+// parseProfile reads a cover.out profile into module-relative path -> counts.
+// COVERED is the statements in blocks hit at least once; TOTAL is all statements.
+func parseProfile(profile, module string) map[string]counts {
+ f, err := os.Open(profile)
+ if err != nil {
+ die("coverage profile not found: %s", profile)
+ }
+ defer f.Close()
+ acc := map[string]counts{}
+ sc := bufio.NewScanner(f)
+ for sc.Scan() {
+ line := strings.TrimSpace(sc.Text())
+ if line == "" || strings.HasPrefix(line, "mode:") {
+ continue
+ }
+ // "<path>:<start>.<col>,<end>.<col> <numStmts> <count>"
+ fields := strings.Fields(line)
+ if len(fields) != 3 {
+ continue
+ }
+ idx := strings.LastIndex(fields[0], ":")
+ if idx < 0 {
+ continue
+ }
+ rel := stripModule(fields[0][:idx], module)
+ ns, err1 := strconv.Atoi(fields[1])
+ cnt, err2 := strconv.Atoi(fields[2])
+ if err1 != nil || err2 != nil {
+ continue
+ }
+ c := acc[rel]
+ c.total += ns
+ if cnt > 0 {
+ c.covered += ns
+ }
+ acc[rel] = c
+ }
+ if err := sc.Err(); err != nil {
+ die("reading %s: %v", profile, err)
+ }
+ return acc
+}
+
+// underDir keeps the entries whose path is at or below sourceRel.
+func underDir(table map[string]counts, sourceRel string) map[string]counts {
+ if sourceRel == "." || sourceRel == "" {
+ return table
+ }
+ prefix := sourceRel + "/"
+ out := map[string]counts{}
+ for path, c := range table {
+ if path == sourceRel || strings.HasPrefix(path, prefix) {
+ out[path] = c
+ }
+ }
+ return out
+}
+
+// sourceFiles returns *.go files under sourceDir (relative to root), skipping
+// test files, vendored code, and the local tooling footprint.
+func sourceFiles(sourceDir, root string) []string {
+ var out []string
+ _ = filepath.WalkDir(sourceDir, func(path string, d os.DirEntry, err error) error {
+ if err != nil {
+ return nil
+ }
+ if d.IsDir() {
+ switch d.Name() {
+ case "vendor", ".git", ".claude", "testdata":
+ return filepath.SkipDir
+ }
+ return nil
+ }
+ name := d.Name()
+ if !strings.HasSuffix(name, ".go") || strings.HasSuffix(name, "_test.go") {
+ return nil
+ }
+ rel, err := filepath.Rel(root, path)
+ if err == nil {
+ out = append(out, filepath.ToSlash(rel))
+ }
+ return nil
+ })
+ sort.Strings(out)
+ return out
+}
+
+func missing(tracked map[string]counts, sourceDir, root string) []string {
+ var out []string
+ for _, f := range sourceFiles(sourceDir, root) {
+ if _, ok := tracked[f]; !ok {
+ out = append(out, f)
+ }
+ }
+ return out
+}
+
+func filePct(c counts) float64 {
+ if c.total > 0 {
+ return 100.0 * float64(c.covered) / float64(c.total)
+ }
+ return 100.0
+}
+
+func projectPct(tracked map[string]counts, miss []string) float64 {
+ total := len(tracked) + len(miss)
+ if total == 0 {
+ return 0.0
+ }
+ score := 0.0
+ for _, c := range tracked {
+ score += filePct(c)
+ }
+ return score / float64(total)
+}
+
+func summaryText(tracked map[string]counts, miss []string, sourceRel string) string {
+ total := len(tracked) + len(miss)
+ pct := projectPct(tracked, miss)
+ var b strings.Builder
+ fmt.Fprintf(&b, "Coverage summary for %s\n\n", sourceRel)
+ fmt.Fprintf(&b, "Project coverage: %.1f%% (%d tracked, %d missing, %d total; missing files count as 0%%)\n\n",
+ pct, len(tracked), len(miss), total)
+ plural := "s"
+ if len(miss) == 1 {
+ plural = ""
+ }
+ fmt.Fprintf(&b, "Not in coverage report: %d file%s\n", len(miss), plural)
+ if len(miss) > 0 {
+ b.WriteString("These files had no coverage entry; they count as 0% in project coverage.\n")
+ sort.Strings(miss)
+ for _, m := range miss {
+ fmt.Fprintf(&b, " %s\n", m)
+ }
+ } else {
+ b.WriteString("Every source file appears in the coverage report.\n")
+ }
+ b.WriteString("\n(Per-file table: run `go tool cover -func=<profile>`.)\n")
+ return b.String()
+}
+
+func main() {
+ args := os.Args[1:]
+ if len(args) < 2 || len(args) > 3 {
+ fmt.Fprintln(os.Stderr, "usage: coverage-summary.go PROFILE SOURCE_DIR [PROJECT_ROOT]")
+ os.Exit(2)
+ }
+ profile, sourceDir := args[0], args[1]
+ root := "."
+ if len(args) == 3 {
+ root = args[2]
+ }
+ absRoot, err := filepath.Abs(root)
+ if err != nil {
+ die("bad project root %s: %v", root, err)
+ }
+ absSource, err := filepath.Abs(sourceDir)
+ if err != nil {
+ die("bad source dir %s: %v", sourceDir, err)
+ }
+ sourceRel, err := filepath.Rel(absRoot, absSource)
+ if err != nil {
+ die("source dir %s is not under project root %s", sourceDir, root)
+ }
+ sourceRel = filepath.ToSlash(sourceRel)
+
+ module := readModule(absRoot)
+ tracked := underDir(parseProfile(profile, module), sourceRel)
+ miss := missing(tracked, absSource, absRoot)
+
+ fmt.Println()
+ fmt.Print(summaryText(tracked, miss, sourceRel))
+}
diff --git a/languages/go/coverage-makefile.txt b/languages/go/coverage-makefile.txt
new file mode 100644
index 0000000..7aba9e9
--- /dev/null
+++ b/languages/go/coverage-makefile.txt
@@ -0,0 +1,43 @@
+# Go coverage — Makefile fragment + setup recommendation
+#
+# This file is owned by the project, not the rulesets bundle. The bundle never
+# edits your Makefile. Copy the two targets below into your own Makefile (and
+# adjust the variables at the top), then delete this file or keep it as a note.
+#
+# What you get:
+# make coverage runs the suite with a coverage profile and prints
+# `go tool cover -func`'s own per-function table
+# make coverage-summary prints a file-weighted project number and every
+# source file on disk absent from the profile, at 0%.
+#
+# Why the summary matters: it weights by file and counts a file absent from the
+# profile as 0%, so an untested file stays visible instead of being averaged
+# away. Note that modern `go test ./...` already lists every module package in
+# the profile (at 0% when untested), so for in-module code the missing-file list
+# is usually empty; it earns its keep on files outside the test compilation
+# (build-tagged files, a dir not covered by ./...). The summary does not
+# reimplement the per-function table — `go tool cover -func` already prints it.
+#
+# ---------------------------------------------------------------------------
+# Prerequisite: none beyond the Go toolchain. `go test -coverprofile` and
+# `go tool cover` ship with Go; the summary script is pure standard library.
+# ---------------------------------------------------------------------------
+
+# Variables — adjust to your layout.
+GO ?= go
+SOURCE_DIR ?= .
+COVERAGE_FILE ?= cover.out
+# The summary script ships with the bundle under .claude/scripts/ (gitignored).
+COVERAGE_SUMMARY ?= .claude/scripts/coverage-summary.go
+
+coverage:
+ @$(GO) test ./... -coverprofile=$(COVERAGE_FILE)
+ @$(GO) tool cover -func=$(COVERAGE_FILE)
+ @$(MAKE) coverage-summary
+
+coverage-summary:
+ @if [ ! -f $(COVERAGE_FILE) ]; then \
+ echo "[!] No coverage file at $(COVERAGE_FILE). Run 'make coverage' first."; \
+ exit 1; \
+ fi
+ @$(GO) run $(COVERAGE_SUMMARY) $(COVERAGE_FILE) $(SOURCE_DIR) .
diff --git a/languages/go/gitignore-add.txt b/languages/go/gitignore-add.txt
new file mode 100644
index 0000000..d9efa67
--- /dev/null
+++ b/languages/go/gitignore-add.txt
@@ -0,0 +1,9 @@
+# Claude Code — local tooling, delivered by install/sync, not committed
+.claude/
+CLAUDE.md
+githooks/
+
+# Go coverage + build artifacts (generated)
+cover.out
+coverage.out
+*.test
diff --git a/languages/go/tests/coverage_summary_test.go b/languages/go/tests/coverage_summary_test.go
new file mode 100644
index 0000000..6b2fb44
--- /dev/null
+++ b/languages/go/tests/coverage_summary_test.go
@@ -0,0 +1,142 @@
+// Black-box tests for the Go bundle coverage-summary script.
+//
+// The script ships into a project's .claude/scripts/ and runs via `go run`, so
+// these tests exercise the real CLI rather than importing it: build a throwaway
+// module (go.mod + source files + a cover.out profile), run the script against
+// it, and assert on stdout. Keeping the test in its own module here means the
+// shipped script dir stays test-free.
+//
+// Normal / Boundary / Error coverage at the behavior level: missing-file
+// detection, the file-weighted number, all-tracked, ignoring _test.go, and a
+// missing-report error.
+package gocovtest
+
+import (
+ "os"
+ "os/exec"
+ "path/filepath"
+ "runtime"
+ "strings"
+ "testing"
+)
+
+// scriptPath resolves coverage-summary.go relative to this test file.
+func scriptPath(t *testing.T) string {
+ t.Helper()
+ _, thisFile, _, ok := runtime.Caller(0)
+ if !ok {
+ t.Fatal("cannot locate test file")
+ }
+ p := filepath.Join(filepath.Dir(thisFile), "..", "claude", "scripts", "coverage-summary.go")
+ abs, err := filepath.Abs(p)
+ if err != nil {
+ t.Fatal(err)
+ }
+ return abs
+}
+
+// fixture writes a go.mod, the given source files, and a cover.out under a temp
+// module root. sources maps relpath -> contents; profile is the raw cover.out.
+func fixture(t *testing.T, sources map[string]string, profile string) string {
+ t.Helper()
+ root := t.TempDir()
+ write := func(rel, body string) {
+ full := filepath.Join(root, rel)
+ if err := os.MkdirAll(filepath.Dir(full), 0o755); err != nil {
+ t.Fatal(err)
+ }
+ if err := os.WriteFile(full, []byte(body), 0o644); err != nil {
+ t.Fatal(err)
+ }
+ }
+ write("go.mod", "module example.com/m\n\ngo 1.26\n")
+ for rel, body := range sources {
+ write(rel, body)
+ }
+ write("cover.out", profile)
+ return root
+}
+
+// run executes the script against a fixture root, returning combined output + err.
+func run(t *testing.T, root, sourceDir string) (string, error) {
+ t.Helper()
+ cmd := exec.Command("go", "run", scriptPath(t),
+ filepath.Join(root, "cover.out"), filepath.Join(root, sourceDir), root)
+ out, err := cmd.CombinedOutput()
+ return string(out), err
+}
+
+func TestMissingFileSurfacedAndCountedZero(t *testing.T) {
+ root := fixture(t,
+ map[string]string{
+ "calc/calc.go": "package calc\n\nfunc Add(a, b int) int { return a + b }\n",
+ "calc/untested.go": "package calc\n\nfunc Mul(a, b int) int { return a * b }\n",
+ },
+ // calc.go fully covered (2/2 stmts); untested.go absent from the profile.
+ "mode: set\nexample.com/m/calc/calc.go:3.40,3.56 2 1\n",
+ )
+ out, err := run(t, root, ".")
+ if err != nil {
+ t.Fatalf("script failed: %v\n%s", err, out)
+ }
+ if !strings.Contains(out, "calc/untested.go") {
+ t.Errorf("missing file not surfaced:\n%s", out)
+ }
+ if !strings.Contains(out, "0%") {
+ t.Errorf("missing-as-0%% note absent:\n%s", out)
+ }
+ // calc.go = 100%, untested.go missing = 0% -> 50.0%
+ if !strings.Contains(out, "50.0%") {
+ t.Errorf("expected project number 50.0%%:\n%s", out)
+ }
+}
+
+func TestAllTrackedNoMissing(t *testing.T) {
+ root := fixture(t,
+ map[string]string{"calc/calc.go": "package calc\n\nfunc Add(a, b int) int { return a + b }\n"},
+ "mode: set\nexample.com/m/calc/calc.go:3.40,3.56 2 1\n",
+ )
+ out, err := run(t, root, ".")
+ if err != nil {
+ t.Fatalf("script failed: %v\n%s", err, out)
+ }
+ if !strings.Contains(out, "Not in coverage report: 0 file") {
+ t.Errorf("expected zero missing files:\n%s", out)
+ }
+ if !strings.Contains(out, "100.0%") {
+ t.Errorf("expected 100.0%% project number:\n%s", out)
+ }
+}
+
+func TestTestFilesAreNotCountedAsSource(t *testing.T) {
+ root := fixture(t,
+ map[string]string{
+ "calc/calc.go": "package calc\n\nfunc Add(a, b int) int { return a + b }\n",
+ "calc/calc_test.go": "package calc\n\nimport \"testing\"\n\nfunc TestX(t *testing.T) {}\n",
+ },
+ "mode: set\nexample.com/m/calc/calc.go:3.40,3.56 2 1\n",
+ )
+ out, err := run(t, root, ".")
+ if err != nil {
+ t.Fatalf("script failed: %v\n%s", err, out)
+ }
+ if strings.Contains(out, "calc_test.go") {
+ t.Errorf("_test.go wrongly counted as source:\n%s", out)
+ }
+ if !strings.Contains(out, "100.0%") {
+ t.Errorf("expected 100.0%% (test file excluded):\n%s", out)
+ }
+}
+
+func TestMissingReportErrors(t *testing.T) {
+ root := fixture(t,
+ map[string]string{"calc/calc.go": "package calc\n"},
+ "mode: set\n",
+ )
+ cmd := exec.Command("go", "run", scriptPath(t),
+ filepath.Join(root, "does-not-exist.out"), root, root)
+ out, err := cmd.CombinedOutput()
+ if err == nil {
+ t.Errorf("expected non-zero exit for missing report; output:\n%s", out)
+ }
+}
diff --git a/languages/go/tests/go.mod b/languages/go/tests/go.mod
new file mode 100644
index 0000000..db72cb1
--- /dev/null
+++ b/languages/go/tests/go.mod
@@ -0,0 +1,3 @@
+module gocovtest
+
+go 1.26
diff --git a/scripts/tests/install-lang.bats b/scripts/tests/install-lang.bats
index a523186..a9f3bfe 100644
--- a/scripts/tests/install-lang.bats
+++ b/scripts/tests/install-lang.bats
@@ -68,3 +68,15 @@ teardown() {
grep -qxF ".claude/" "$PROJECT/.gitignore"
grep -qxF "coverage.json" "$PROJECT/.gitignore"
}
+
+@test "install-lang go: coverage-only slice lands without a CLAUDE.md" {
+ run bash "$INSTALL_LANG" go "$PROJECT"
+
+ [ "$status" -eq 0 ]
+ [ -f "$PROJECT/.claude/scripts/coverage-summary.go" ]
+ [ -f "$PROJECT/coverage-makefile.txt" ]
+ grep -qxF ".claude/" "$PROJECT/.gitignore"
+ grep -qxF "cover.out" "$PROJECT/.gitignore"
+ # The slice ships no rules of its own, so there is no Go CLAUDE.md to seed.
+ [ ! -f "$PROJECT/CLAUDE.md" ]
+}
diff --git a/todo.org b/todo.org
index 9ff1175..b6f28b8 100644
--- a/todo.org
+++ b/todo.org
@@ -1156,19 +1156,18 @@ Reference (dotemacs): =scripts/coverage-summary.el=, =modules/coverage-core.el=,
Origin: handoff from the .emacs.d session, 2026-05-25.
-** TODO [#C] Fan out coverage-summary to Go and TypeScript bundles :feature:
+** TODO [#C] Fan out coverage-summary to the TypeScript bundle :feature:
:PROPERTIES:
:CREATED: [2026-05-31 Sun]
:END:
-The Elisp pilot proved the pattern; Python followed (both DONE above). Python confirmed the plumbing is genuinely generic — =sync-language-bundle.sh= auto-fixes any =claude/scripts/*= and inbox-drops any =coverage-makefile.txt=; =install-lang.sh= seeds the fragment; =make test= now discovers both =languages/*/tests/test-*.el= (ERT) and =languages/*/tests/test_*.py= (pytest). So Go and TS are each just: the parser script, its tests, and the fragment.
+The Elisp pilot proved the pattern; Python and Go followed (all three DONE above). The plumbing is generic: =install-lang.sh= seeds the fragment, and =make test= now discovers ERT (=test-*.el=), pytest (=test_*.py=), and =go test= (=*_test.go=) under =languages/*/tests/=. TypeScript is the last one.
-- Go: =go test -coverprofile=cover.out=; parse =cover.out= (simple text), or =go tool cover -func=. Note Go has no =make test= discovery path yet — add a =go test= runner for =languages/go/tests= when this lands.
-- TypeScript/JS: nyc/Istanbul =coverage-final.json= / json-summary. Needs a JS test-discovery path in =make test= too.
+- TypeScript/JS: nyc/Istanbul =coverage-final.json= / =coverage-summary.json=. Same kernel: file-weighted project number, on-disk =*.ts=/=*.js= absent from the report counted as 0%. nyc prints its own table, so the script focuses on the missing-file list and the number. Needs a vitest/jest (or =node --test=) discovery path in =make test=, mirroring the go-test block.
-Keep the kernel identical: file-weighted project number, source files absent from the report counted as 0%. Don't reimplement the per-file table where the built-in reporter already prints one — Go's =go tool cover -func= and nyc both do, so those scripts focus on the missing-file list and the project number.
-
-Python notes for the next person: the script parses coverage.py's =files[path].summary.{covered_lines,num_statements}= (stable since coverage 5.x), resolves report paths against the report's parent dir (= project root), recurses the source dir for =*.py=, and was proven against a synthetic report matching the documented schema — not yet against a live =coverage json= run (coverage.py wasn't installed in the rulesets env). First real adopter should sanity-check against an actual report.
+Notes for the next person, from the Python + Go runs:
+- Python: parses coverage.py's =files[path].summary.{covered_lines,num_statements}= (stable since coverage 5.x), resolves report paths against the report's parent dir. Proven against a synthetic report, not a live =coverage json= run (coverage.py wasn't installed). Sanity-check against a real one.
+- Go: =languages/go/= is a coverage-only slice with no rule file, so =sync-language-bundle.sh= can't fingerprint it (detection keys on a bundle's own =.claude/rules/*.md=). The script is delivered by =make install-lang LANG=go= but is not sync-maintained until the Go bundle gets a real rule file + =CLAUDE.md=. Building out that bundle is the natural companion task. Also: modern =go test ./...= already lists every module package in the profile at 0%, so the missing-file list is usually empty for in-module code; it earns its keep on build-tagged files and dirs outside =./...=.
** TODO [#B] Cross-project pattern catalog :spec:thinking:
:PROPERTIES: