@theglitchking/babel-fish 2.4.3 → 2.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -98,6 +98,15 @@ POST-INSTALL COMMANDS
98
98
  python .claude/project-map/mine-sessions.py --verbose
99
99
  python .claude/project-map/mine-sessions.py --dry-run
100
100
 
101
+ Check auto-loaded context (CLAUDE.md + .claude/rules/) — budget, pointers:
102
+ python .claude/project-map/context-check.py
103
+ python .claude/project-map/context-check.py --budget 80000 --map-style
104
+
105
+ Check files land where .claude/structure.toml says (opt-in; see --help):
106
+ python .claude/project-map/structure-check.py # no manifest: explains the options
107
+ python .claude/project-map/structure-check.py --detect # repo facts: mode, environments, folders
108
+ python .claude/project-map/structure-check.py --bootstrap # write a starting manifest (never overwrites)
109
+
101
110
  Re-install git hooks (if you cloned a fresh copy):
102
111
  bash .githooks/install.sh
103
112
 
@@ -121,7 +130,8 @@ KEY FILES AFTER INSTALL
121
130
  .claude/rules/project-vocabulary.md — Auto-loaded every session
122
131
  .claude/rules/operational-runbook.md — Edit manually to grow over time
123
132
  .claude/skills/<slug>-developer-skill/ — Your developer skill
124
- .githooks/pre-commit — Auto-regenerates map on commit
133
+ .githooks/pre-commit — Regenerates map; checks auto-loaded context + structure
134
+ .claude/templates/structure/*.toml — Layout templates (single, monorepo, multi-repo)
125
135
 
126
136
  GRADING CATEGORIES (90% to pass)
127
137
  Section completeness 25% — All 19 sections generated
@@ -378,17 +388,73 @@ if echo "$STAGED_FILES" | grep -qE "$EXTENSIONS_PATTERN" 2>/dev/null; then
378
388
  fi
379
389
  # ── End Codebase Mapper ──────────────────────────────────────────────────────'
380
390
 
381
- if [ -f "$pre_commit" ]; then
382
- if ! grep -q 'Codebase Mapper' "$pre_commit" 2>/dev/null; then
383
- { echo ""; echo "$hook_snippet"; } >> "$pre_commit"
384
- ok "Appended to existing pre-commit hook"
385
- else
386
- ok "pre-commit hook already contains Codebase Mapper snippet"
391
+ # Own marker, checked separately: hooks installed before #20 already carry
392
+ # the Codebase Mapper block, and re-running the installer must still add this.
393
+ local check_snippet
394
+ check_snippet=$(cat <<'SNIPPET'
395
+ # ── Context Check: auto-loaded instructions stay in budget, pointers resolve ──
396
+ if git diff --cached --name-only 2>/dev/null | grep -qE '^(\.claude/)?CLAUDE(\.local)?\.md$|^\.claude/rules/.*\.md$'; then
397
+ CHECK_SCRIPT=".claude/project-map/context-check.py"
398
+ if [ -f "$CHECK_SCRIPT" ]; then
399
+ PYTHON=""
400
+ if [ -f ".venv/bin/python3" ]; then PYTHON=".venv/bin/python3"
401
+ elif command -v python3 &>/dev/null; then PYTHON="python3"
402
+ elif command -v python &>/dev/null; then PYTHON="python"
387
403
  fi
388
- else
389
- printf '#!/bin/bash\n%s\n' "$hook_snippet" > "$pre_commit"
404
+ if [ -n "$PYTHON" ] && ! $PYTHON "$CHECK_SCRIPT"; then
405
+ echo "[context-check] Commit blocked. Fix the FAIL lines above, or add flags (--budget N, --map-style) to this call in .githooks/pre-commit." >&2
406
+ exit 1
407
+ fi
408
+ fi
409
+ fi
410
+ # ── End Context Check ────────────────────────────────────────────────────────
411
+ SNIPPET
412
+ )
413
+
414
+ local structure_snippet
415
+ structure_snippet=$(cat <<'SNIPPET'
416
+ # ── Structure Check: files land where .claude/structure.toml says (#22) ──────
417
+ # Opt-in: runs only once the repo has a manifest. On Python < 3.11 it warns and passes.
418
+ if [ -f ".claude/structure.toml" ]; then
419
+ CHECK_SCRIPT=".claude/project-map/structure-check.py"
420
+ if [ -f "$CHECK_SCRIPT" ]; then
421
+ PYTHON=""
422
+ if [ -f ".venv/bin/python3" ]; then PYTHON=".venv/bin/python3"
423
+ elif command -v python3 &>/dev/null; then PYTHON="python3"
424
+ elif command -v python &>/dev/null; then PYTHON="python"
425
+ fi
426
+ if [ -n "$PYTHON" ] && ! $PYTHON "$CHECK_SCRIPT" --staged; then
427
+ echo "[structure-check] Commit blocked. Each FAIL above has a fix: line -- or change .claude/structure.toml if the structure should change." >&2
428
+ exit 1
429
+ fi
430
+ fi
431
+ fi
432
+ # ── End Structure Check ──────────────────────────────────────────────────────
433
+ SNIPPET
434
+ )
435
+
436
+ if [ ! -f "$pre_commit" ]; then
437
+ printf '#!/bin/bash\n' > "$pre_commit"
390
438
  ok "Created pre-commit hook"
391
439
  fi
440
+ if ! grep -q 'Codebase Mapper' "$pre_commit" 2>/dev/null; then
441
+ { echo ""; echo "$hook_snippet"; } >> "$pre_commit"
442
+ ok "Added Codebase Mapper to pre-commit hook"
443
+ else
444
+ ok "pre-commit hook already contains Codebase Mapper snippet"
445
+ fi
446
+ if ! grep -q 'Context Check' "$pre_commit" 2>/dev/null; then
447
+ { echo ""; echo "$check_snippet"; } >> "$pre_commit"
448
+ ok "Added Context Check to pre-commit hook"
449
+ else
450
+ ok "pre-commit hook already contains Context Check snippet"
451
+ fi
452
+ if ! grep -q 'Structure Check' "$pre_commit" 2>/dev/null; then
453
+ { echo ""; echo "$structure_snippet"; } >> "$pre_commit"
454
+ ok "Added Structure Check to pre-commit hook"
455
+ else
456
+ ok "pre-commit hook already contains Structure Check snippet"
457
+ fi
392
458
 
393
459
  chmod +x "$pre_commit"
394
460
 
@@ -480,7 +546,7 @@ if $DRY_RUN; then
480
546
  _dry "mkdir -p $CLAUDE_DIR/rules"
481
547
  _dry "mkdir -p $CLAUDE_DIR/skills/<project>-developer-skill/"
482
548
  _dry "copy scripts/*.sh → $CLAUDE_DIR/scripts/"
483
- _dry "copy templates/*.template → $CLAUDE_DIR/templates/"
549
+ _dry "copy templates/*.template, templates/structure/*.toml → $CLAUDE_DIR/templates/"
484
550
  _dry "copy project-map/*.py → $CLAUDE_DIR/project-map/"
485
551
  _dry "write $CLAUDE_DIR/project-map/PROJECT_MAP.md"
486
552
  _dry "write $CLAUDE_DIR/project-map/sections/01-vocabulary.md (+ 18 more sections)"
@@ -512,6 +578,9 @@ if [ "$PLUGIN_SOURCE_DIR" != "$CLAUDE_DIR" ]; then
512
578
  cp -r "$PLUGIN_SOURCE_DIR/project-map/generate.py" "$CLAUDE_DIR/project-map/" 2>/dev/null || true
513
579
  cp -r "$PLUGIN_SOURCE_DIR/project-map/grader.py" "$CLAUDE_DIR/project-map/" 2>/dev/null || true
514
580
  cp -r "$PLUGIN_SOURCE_DIR/project-map/mine-sessions.py" "$CLAUDE_DIR/project-map/" 2>/dev/null || true
581
+ cp -r "$PLUGIN_SOURCE_DIR/project-map/context-check.py" "$CLAUDE_DIR/project-map/" 2>/dev/null || true
582
+ cp -r "$PLUGIN_SOURCE_DIR/project-map/structure-check.py" "$CLAUDE_DIR/project-map/" 2>/dev/null || true
583
+ # Never copies .claude/structure.toml: the manifest is the repo's own data (#22).
515
584
  fi
516
585
 
517
586
  # ── Step 1: Ensure Python ─────────────────────────────────────────────────────
@@ -0,0 +1,156 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ context-check.py — Babel Fish
4
+ Keeps the files Claude Code auto-loads at session start small and pointing at
5
+ things that exist (#20).
6
+
7
+ `CLAUDE.md` and every `.claude/rules/*.md` load into every session. Past the
8
+ harness limit (150k chars) rules stop binding and nothing reports it; before
9
+ that, growth is one paragraph at a time and nobody sees the total. Three checks:
10
+
11
+ 1. BUDGET — total chars under --budget (default 60k, well under the limit).
12
+ 2. POINTERS — every `→ `path``, backticked `.documentation/…md` path and
13
+ relative markdown link names a file that exists. hewtd link-checks
14
+ only INSIDE `.documentation/`, so archiving or renaming a doc a
15
+ rule points at breaks the pointer silently.
16
+ 3. RULE LINES (--map-style only) — in a rules file every bullet is
17
+ `- ALWAYS|NEVER <rule> — <why> → `doc``; in CLAUDE.md every
18
+ bullet with a pointer is. Opt-in: it is one repo's convention, and
19
+ this runs from the pre-commit hook of every babel-fish install.
20
+
21
+ Deliberately NOT part of generate.py: the hook runs generate.py only when code
22
+ is staged, `.claude/` is outside the watch set, and the hook discards its
23
+ errors — a failure there could never block a commit.
24
+
25
+ Usage:
26
+ python context-check.py [--budget N] [--map-style] [--descriptive a.md,b.md]
27
+ [--warn-only] [--project-root PATH]
28
+ """
29
+ from __future__ import annotations
30
+
31
+ import argparse
32
+ import re
33
+ import sys
34
+ from pathlib import Path
35
+
36
+ PROJECT_ROOT = Path(__file__).parent.parent.parent
37
+
38
+ HARNESS_LIMIT = 150_000
39
+ DEFAULT_BUDGET = 60_000
40
+ # Maps whose lines describe (tools, traps, vocabulary) rather than rule — exempt
41
+ # from the ALWAYS/NEVER form. project-vocabulary.md is babel-fish's own output.
42
+ DEFAULT_DESCRIPTIVE = {"operational-runbook.md", "project-vocabulary.md", "tool-registry.md"}
43
+
44
+ ARROW_RE = re.compile(r"→ `([^`\s*]+?\.md)(?:#[^`]*)?`")
45
+ DOC_PATH_RE = re.compile(r"`(\.documentation/[^`\s*]+?\.md)(?:#[^`]*)?`")
46
+ MD_LINK_RE = re.compile(r"\]\(([^)\s]+)\)")
47
+ FENCE_RE = re.compile(r"^```.*?^```", re.MULTILINE | re.DOTALL)
48
+ RULE_RE = re.compile(r"- (ALWAYS|NEVER)\b")
49
+
50
+
51
+ def configure_paths(project_root: Path) -> None:
52
+ """Read root. Nothing here writes, but match the sibling scripts (#18)."""
53
+ global PROJECT_ROOT
54
+ PROJECT_ROOT = project_root
55
+
56
+
57
+ def loaded() -> dict[str, str]:
58
+ """-> {relpath: text} for every file Claude Code loads at session start."""
59
+ paths = [PROJECT_ROOT / n for n in ("CLAUDE.md", "CLAUDE.local.md", ".claude/CLAUDE.md")]
60
+ # ponytail: rules with `paths:` frontmatter load lazily but are counted anyway —
61
+ # over-counting errs toward the budget, not past the limit.
62
+ paths += sorted((PROJECT_ROOT / ".claude" / "rules").rglob("*.md"))
63
+ return {p.relative_to(PROJECT_ROOT).as_posix(): p.read_text(encoding="utf-8", errors="replace")
64
+ for p in paths if p.is_file()}
65
+
66
+
67
+ def bullets(text: str) -> list[str]:
68
+ return [ln for ln in text.split("\n") if ln.startswith("- ")]
69
+
70
+
71
+ def bad_rule_lines(text: str) -> list[str]:
72
+ """-> bullets that are not `- ALWAYS|NEVER … → `path``."""
73
+ return [ln for ln in bullets(text) if not RULE_RE.match(ln) or not ARROW_RE.search(ln)]
74
+
75
+
76
+ def unresolved(text: str, file_dir: Path) -> list[str]:
77
+ """-> every pointer in `text` naming a path that does not exist.
78
+
79
+ `→` pointers and backticked `.documentation/` paths are root-relative (that is
80
+ how the map convention writes them); markdown links resolve relative to the file or
81
+ to the root. Code fences are skipped — examples are not pointers.
82
+ """
83
+ text = FENCE_RE.sub("", text)
84
+ missing = [p for p in set(ARROW_RE.findall(text)) | set(DOC_PATH_RE.findall(text))
85
+ if not (PROJECT_ROOT / p).exists()]
86
+ for link in MD_LINK_RE.findall(text):
87
+ target = link.split("#", 1)[0]
88
+ if not target or re.match(r"[a-z][a-z0-9+.-]*:", target, re.I): # anchor-only, http:, mailto:
89
+ continue
90
+ target = target.lstrip("/")
91
+ # File-relative is markdown; root-relative is how Claude reads a path, and
92
+ # what the installer's own .claude/CLAUDE.md pointer uses. Either counts.
93
+ if not ((file_dir / target).exists() or (PROJECT_ROOT / target).exists()):
94
+ missing.append(link)
95
+ return sorted(set(missing))
96
+
97
+
98
+ def run(budget: int, map_style: bool, descriptive: set[str]) -> int:
99
+ """Print the report; return the number of failed checks."""
100
+ fails = 0
101
+
102
+ def check(name: str, ok: bool, detail: str = "") -> None:
103
+ nonlocal fails
104
+ print(f" {'PASS' if ok else 'FAIL'} {name}" + (f" -- {detail}" if detail and not ok else ""))
105
+ fails += not ok
106
+
107
+ files = loaded()
108
+ total = sum(len(t) for t in files.values())
109
+
110
+ print("\nthe budget")
111
+ check(f"auto-loaded context is under {budget:,} chars", total <= budget,
112
+ f"{total:,} -- move detail into .documentation/ and leave one line pointing at it")
113
+ for k, t in sorted(files.items(), key=lambda kv: -len(kv[1])):
114
+ print(f" {len(t):>7,} {k}")
115
+ print(f" {total:>7,} total ({total / HARNESS_LIMIT:.0%} of the {HARNESS_LIMIT:,} harness limit)")
116
+
117
+ print("\nevery pointer resolves")
118
+ for k, t in files.items():
119
+ miss = unresolved(t, (PROJECT_ROOT / k).parent)
120
+ check(f"{k}: all pointers exist", not miss, ", ".join(miss[:3]))
121
+
122
+ if map_style:
123
+ print("\nrule lines are ALWAYS/NEVER, with a pointer")
124
+ for k, t in files.items():
125
+ if Path(k).name in descriptive:
126
+ continue
127
+ if k.startswith(".claude/rules/"):
128
+ bad = bad_rule_lines(t)
129
+ check(f"{k}: every bullet is ALWAYS|NEVER → doc", not bad, bad[0][:80] if bad else "")
130
+ else: # CLAUDE.md: tool lines may go unpointed; a pointed bullet is a rule
131
+ bad = [ln for ln in bad_rule_lines(t) if "→" in ln]
132
+ check(f"{k}: every pointed bullet is ALWAYS|NEVER", not bad, bad[0][:80] if bad else "")
133
+
134
+ print(f"\n{'OK' if not fails else f'{fails} FAILED'}")
135
+ return fails
136
+
137
+
138
+ def main() -> None:
139
+ ap = argparse.ArgumentParser(description="Check the context Claude Code auto-loads (#20).")
140
+ ap.add_argument("--budget", type=int, default=DEFAULT_BUDGET,
141
+ help=f"max total chars (default {DEFAULT_BUDGET:,}; harness limit {HARNESS_LIMIT:,})")
142
+ ap.add_argument("--map-style", action="store_true",
143
+ help="also require rule bullets to be `- ALWAYS|NEVER … → `doc``")
144
+ ap.add_argument("--descriptive", default=",".join(sorted(DEFAULT_DESCRIPTIVE)),
145
+ help="comma-separated filenames exempt from --map-style (default: %(default)s)")
146
+ ap.add_argument("--warn-only", action="store_true", help="report, but always exit 0")
147
+ ap.add_argument("--project-root", type=Path, default=None)
148
+ args = ap.parse_args()
149
+ if args.project_root:
150
+ configure_paths(args.project_root.resolve())
151
+ fails = run(args.budget, args.map_style, {n.strip() for n in args.descriptive.split(",") if n.strip()})
152
+ sys.exit(1 if fails and not args.warn_only else 0)
153
+
154
+
155
+ if __name__ == "__main__":
156
+ main()
@@ -1522,6 +1522,30 @@ def write_glossary(vocab: list[dict], stack: dict) -> Path:
1522
1522
  # ║ PROJECT MAP TOC ║
1523
1523
  # ╚══════════════════════════════════════════════════════════════════════════╝
1524
1524
 
1525
+ def build_structure_pointer() -> list[str]:
1526
+ """Where files go (#22): a pointer to the manifest, never a copy of it."""
1527
+ manifest = PROJECT_ROOT / '.claude' / 'structure.toml'
1528
+ if not manifest.is_file():
1529
+ return []
1530
+ lines = [
1531
+ "",
1532
+ "## Repo Structure\n",
1533
+ "Where files go — approved folders, environments, related repos: "
1534
+ "`.claude/structure.toml`. Checked on commit by `structure-check.py`.",
1535
+ ]
1536
+ try:
1537
+ import tomllib
1538
+ m = tomllib.loads(manifest.read_text(encoding='utf-8'))
1539
+ except Exception: # Python < 3.11 or a broken manifest: the pointer alone still helps
1540
+ return lines
1541
+ repos = [r for r in m.get('repo', []) if isinstance(r, dict)]
1542
+ if repos:
1543
+ lines.append("\nRelated repos — each keeps its own map:\n")
1544
+ lines += [f"- **{r.get('name', '?')}** `{r.get('path') or r.get('url', '')}`"
1545
+ + (f" — {r['purpose']}" if r.get('purpose') else '') for r in repos]
1546
+ return lines
1547
+
1548
+
1525
1549
  def build_project_map(
1526
1550
  stack: dict,
1527
1551
  routes: list[dict],
@@ -1596,6 +1620,7 @@ def build_project_map(
1596
1620
  when = WHEN_TO_READ.get(num, '')
1597
1621
  lines.append(f"| [{num}](sections/{path.name}) | {display} | {size_kb:.1f} KB | {when} |")
1598
1622
 
1623
+ lines += build_structure_pointer()
1599
1624
  lines += [
1600
1625
  "",
1601
1626
  "## Quick Routing\n",