@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.
- package/.claude/install.sh +79 -10
- package/.claude/project-map/context-check.py +156 -0
- package/.claude/project-map/generate.py +25 -0
- package/.claude/project-map/structure-check.py +773 -0
- package/.claude/project-map/test_context_check.py +221 -0
- package/.claude/project-map/test_generate.py +13 -0
- package/.claude/project-map/test_structure_check.py +864 -0
- package/.claude/templates/structure/monorepo.toml +76 -0
- package/.claude/templates/structure/multi-repo.toml +85 -0
- package/.claude/templates/structure/single.toml +73 -0
- package/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/.githooks/install.sh +0 -0
- package/.githooks/pre-commit +35 -0
- package/CHANGELOG.md +96 -0
- package/README.md +49 -3
- package/checksums.json +1 -1
- package/package.json +6 -2
- package/scripts/link-skills.js +3 -3
- package/skills/structure-bootstrap/SKILL.md +209 -0
package/.claude/install.sh
CHANGED
|
@@ -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 —
|
|
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
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
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
|
-
|
|
389
|
-
|
|
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",
|