@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
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Regression tests for context-check.py — run: python3 .claude/project-map/test_context_check.py
|
|
3
|
+
|
|
4
|
+
Stdlib assert + __main__, same shape as the sibling suites. Every check is proven
|
|
5
|
+
to DETECT something (a seeded bad input) as well as to pass a good one: a check
|
|
6
|
+
that only ever prints PASS is indistinguishable from one that checks nothing.
|
|
7
|
+
|
|
8
|
+
Every fixture is a temp project passed via --project-root, so nothing here reads
|
|
9
|
+
or writes the real repo (#18).
|
|
10
|
+
"""
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import inspect
|
|
14
|
+
import shutil
|
|
15
|
+
import subprocess
|
|
16
|
+
import sys
|
|
17
|
+
import tempfile
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
|
|
20
|
+
HERE = Path(__file__).parent
|
|
21
|
+
SCRIPT = HERE / "context-check.py"
|
|
22
|
+
REPO = HERE.parent.parent
|
|
23
|
+
|
|
24
|
+
GOOD_RULES = "# Rules\n\n- NEVER do the bad thing — it breaks. → `.documentation/a.md`\n"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def make_project(root: Path, claude_md: str = "# Project\n", rules: dict[str, str] | None = None) -> Path:
|
|
28
|
+
(root / ".documentation").mkdir(parents=True, exist_ok=True)
|
|
29
|
+
(root / ".documentation/a.md").write_text("# A\n")
|
|
30
|
+
(root / "CLAUDE.md").write_text(claude_md)
|
|
31
|
+
(root / ".claude/rules").mkdir(parents=True, exist_ok=True)
|
|
32
|
+
for name, text in (rules or {}).items():
|
|
33
|
+
(root / ".claude/rules" / name).write_text(text)
|
|
34
|
+
return root
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def check(root: Path, *flags: str) -> subprocess.CompletedProcess:
|
|
38
|
+
return subprocess.run([sys.executable, str(SCRIPT), "--project-root", str(root), *flags],
|
|
39
|
+
capture_output=True, text=True)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
# ── budget ───────────────────────────────────────────────────────────────────
|
|
43
|
+
|
|
44
|
+
def test_under_budget_passes(tmp):
|
|
45
|
+
r = check(make_project(tmp, rules={"r.md": GOOD_RULES}))
|
|
46
|
+
assert r.returncode == 0, r.stdout
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def test_over_budget_fails_and_names_the_file(tmp):
|
|
50
|
+
make_project(tmp, rules={"huge.md": "x" * 2_000})
|
|
51
|
+
r = check(tmp, "--budget", "1000")
|
|
52
|
+
assert r.returncode == 1, r.stdout
|
|
53
|
+
assert "FAIL auto-loaded context is under 1,000" in r.stdout
|
|
54
|
+
assert ".claude/rules/huge.md" in r.stdout
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def test_budget_counts_nested_rules_and_dot_claude_claude_md(tmp):
|
|
58
|
+
make_project(tmp)
|
|
59
|
+
(tmp / ".claude/rules/sub").mkdir()
|
|
60
|
+
(tmp / ".claude/rules/sub/deep.md").write_text("y" * 600)
|
|
61
|
+
(tmp / ".claude/CLAUDE.md").write_text("z" * 600)
|
|
62
|
+
r = check(tmp, "--budget", "1000")
|
|
63
|
+
assert r.returncode == 1, r.stdout
|
|
64
|
+
assert ".claude/rules/sub/deep.md" in r.stdout and ".claude/CLAUDE.md" in r.stdout
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def test_warn_only_exits_zero(tmp):
|
|
68
|
+
r = check(make_project(tmp, rules={"huge.md": "x" * 2_000}), "--budget", "1000", "--warn-only")
|
|
69
|
+
assert r.returncode == 0 and "FAIL" in r.stdout, r.stdout
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
# ── pointers ─────────────────────────────────────────────────────────────────
|
|
73
|
+
|
|
74
|
+
def test_dangling_arrow_pointer_fails(tmp):
|
|
75
|
+
r = check(make_project(tmp, rules={"r.md": "- NEVER x — y. → `.documentation/gone.md`\n"}))
|
|
76
|
+
assert r.returncode == 1 and ".documentation/gone.md" in r.stdout, r.stdout
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def test_dangling_backticked_doc_path_fails(tmp):
|
|
80
|
+
r = check(make_project(tmp, claude_md="See `.documentation/missing/doc.md` for more.\n"))
|
|
81
|
+
assert r.returncode == 1 and "missing/doc.md" in r.stdout, r.stdout
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def test_dangling_markdown_link_fails_relative_to_the_file(tmp):
|
|
85
|
+
# This repo's runbook links docs this way — the fork's regexes never saw them.
|
|
86
|
+
make_project(tmp, rules={"r.md": "[doc](../../.documentation/a.md) and [bad](../../.documentation/nope.md)\n"})
|
|
87
|
+
r = check(tmp)
|
|
88
|
+
assert r.returncode == 1, r.stdout
|
|
89
|
+
assert "nope.md" in r.stdout and "a.md" not in r.stdout.split("--")[-1], r.stdout
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def test_root_relative_link_in_dot_claude_is_accepted(tmp):
|
|
93
|
+
# The installer writes exactly this into .claude/CLAUDE.md. Strictly file-relative,
|
|
94
|
+
# it blocked the first commit of every fresh install.
|
|
95
|
+
make_project(tmp)
|
|
96
|
+
(tmp / ".claude/project-map").mkdir()
|
|
97
|
+
(tmp / ".claude/project-map/PROJECT_MAP.md").write_text("# Map\n")
|
|
98
|
+
(tmp / ".claude/CLAUDE.md").write_text("[map](.claude/project-map/PROJECT_MAP.md)\n")
|
|
99
|
+
r = check(tmp)
|
|
100
|
+
assert r.returncode == 0, r.stdout
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def test_anchor_url_and_code_fence_are_not_false_alarms(tmp):
|
|
104
|
+
make_project(tmp, claude_md=(
|
|
105
|
+
"→ `.documentation/a.md#section`\n"
|
|
106
|
+
"[site](https://example.com/x.md) [mail](mailto:a@b.c) [here](#top)\n"
|
|
107
|
+
"```md\n→ `.documentation/example-only.md`\n[x](nowhere.md)\n```\n"
|
|
108
|
+
))
|
|
109
|
+
r = check(tmp)
|
|
110
|
+
assert r.returncode == 0, r.stdout
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
# ── rule lines (--map-style) ─────────────────────────────────────────────────
|
|
114
|
+
|
|
115
|
+
def test_rule_lines_ignored_without_map_style(tmp):
|
|
116
|
+
r = check(make_project(tmp, rules={"r.md": "- just some context\n"}))
|
|
117
|
+
assert r.returncode == 0 and "ALWAYS/NEVER" not in r.stdout, r.stdout
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def test_context_line_in_rules_file_fails(tmp):
|
|
121
|
+
r = check(make_project(tmp, rules={"r.md": "- run the gates first → `.documentation/a.md`\n"}), "--map-style")
|
|
122
|
+
assert r.returncode == 1 and "run the gates first" in r.stdout, r.stdout
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def test_rule_without_pointer_fails(tmp):
|
|
126
|
+
r = check(make_project(tmp, rules={"r.md": "- NEVER do it — because.\n"}), "--map-style")
|
|
127
|
+
assert r.returncode == 1, r.stdout
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def test_good_rule_line_passes(tmp):
|
|
131
|
+
r = check(make_project(tmp, rules={"r.md": GOOD_RULES}), "--map-style")
|
|
132
|
+
assert r.returncode == 0, r.stdout
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def test_descriptive_files_are_exempt(tmp):
|
|
136
|
+
make_project(tmp, rules={"operational-runbook.md": "- a trap, described\n"})
|
|
137
|
+
assert check(tmp, "--map-style").returncode == 0
|
|
138
|
+
r = check(tmp, "--map-style", "--descriptive", "other.md")
|
|
139
|
+
assert r.returncode == 1, r.stdout
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def test_claude_md_only_pointed_bullets_must_be_rules(tmp):
|
|
143
|
+
make_project(tmp, claude_md="- `run it` with docker\n")
|
|
144
|
+
assert check(tmp, "--map-style").returncode == 0
|
|
145
|
+
(tmp / "CLAUDE.md").write_text("- a tool line → `.documentation/a.md`\n")
|
|
146
|
+
assert check(tmp, "--map-style").returncode == 1
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
# ── wiring ───────────────────────────────────────────────────────────────────
|
|
150
|
+
|
|
151
|
+
def test_this_repo_passes():
|
|
152
|
+
r = subprocess.run([sys.executable, str(SCRIPT)], capture_output=True, text=True, cwd=REPO)
|
|
153
|
+
assert r.returncode == 0, r.stdout
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def _block(text: str) -> str:
|
|
157
|
+
start = text.index("# ── Context Check")
|
|
158
|
+
end = text.index("# ── End Context Check")
|
|
159
|
+
return text[start:end]
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def test_installer_writes_the_same_hook_block():
|
|
163
|
+
assert _block((REPO / ".githooks/pre-commit").read_text()) == _block((REPO / ".claude/install.sh").read_text())
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def test_hooks_are_committed_executable():
|
|
167
|
+
# Git silently skips a non-executable hook. Until #20 both were committed 100644,
|
|
168
|
+
# so no clone of this repo ever ran its pre-commit hook.
|
|
169
|
+
out = subprocess.run(["git", "ls-files", "-s", ".githooks/"], capture_output=True, text=True,
|
|
170
|
+
cwd=REPO, check=True).stdout
|
|
171
|
+
modes = {line.split("\t")[1]: line.split()[0] for line in out.splitlines()}
|
|
172
|
+
assert modes and all(m == "100755" for m in modes.values()), modes
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def test_hook_blocks_a_bad_rules_commit(tmp):
|
|
176
|
+
git = ["git", "-c", "user.name=t", "-c", "user.email=t@t", "-c", "core.hooksPath=.githooks"]
|
|
177
|
+
subprocess.run(["git", "init", "-q", str(tmp)], check=True)
|
|
178
|
+
make_project(tmp, rules={"r.md": GOOD_RULES})
|
|
179
|
+
(tmp / ".githooks").mkdir()
|
|
180
|
+
shutil.copy(REPO / ".githooks/pre-commit", tmp / ".githooks/pre-commit")
|
|
181
|
+
(tmp / ".githooks/pre-commit").chmod(0o755) # the mode itself is test_hooks_are_committed_executable's job
|
|
182
|
+
(tmp / ".claude/project-map").mkdir(parents=True)
|
|
183
|
+
shutil.copy(SCRIPT, tmp / ".claude/project-map/context-check.py")
|
|
184
|
+
commit = lambda msg: subprocess.run(git + ["commit", "-qm", msg], cwd=tmp, capture_output=True, text=True)
|
|
185
|
+
|
|
186
|
+
subprocess.run(["git", "add", "-A"], cwd=tmp, check=True)
|
|
187
|
+
r = commit("good")
|
|
188
|
+
assert r.returncode == 0, r.stdout + r.stderr
|
|
189
|
+
|
|
190
|
+
(tmp / ".claude/rules/r.md").write_text("- NEVER x — y. → `.documentation/gone.md`\n")
|
|
191
|
+
subprocess.run(["git", "add", "-A"], cwd=tmp, check=True)
|
|
192
|
+
r = commit("bad")
|
|
193
|
+
assert r.returncode != 0, "hook let a dangling pointer through"
|
|
194
|
+
assert "Commit blocked" in r.stderr, r.stderr
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def main() -> int:
|
|
198
|
+
tests = [v for k, v in sorted(globals().items()) if k.startswith("test_")]
|
|
199
|
+
failed = []
|
|
200
|
+
base = Path(tempfile.mkdtemp(prefix="context-check-test-"))
|
|
201
|
+
try:
|
|
202
|
+
for fn in tests:
|
|
203
|
+
d = base / fn.__name__
|
|
204
|
+
d.mkdir(parents=True)
|
|
205
|
+
try:
|
|
206
|
+
if "tmp" in inspect.signature(fn).parameters:
|
|
207
|
+
fn(d)
|
|
208
|
+
else:
|
|
209
|
+
fn()
|
|
210
|
+
print(f" ok {fn.__name__}")
|
|
211
|
+
except Exception as e:
|
|
212
|
+
failed.append(fn.__name__)
|
|
213
|
+
print(f" FAIL {fn.__name__}: {type(e).__name__}: {e}")
|
|
214
|
+
finally:
|
|
215
|
+
shutil.rmtree(base, ignore_errors=True)
|
|
216
|
+
print(f"\n{len(tests) - len(failed)}/{len(tests)} passed")
|
|
217
|
+
return 1 if failed else 0
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
if __name__ == "__main__":
|
|
221
|
+
sys.exit(main())
|
|
@@ -179,6 +179,19 @@ def test_plugin_repo_map_is_not_empty(g):
|
|
|
179
179
|
assert "no vocabulary generated yet" not in section, "section 01 still renders the stub"
|
|
180
180
|
|
|
181
181
|
|
|
182
|
+
# ── Structure pointer (#22) ──────────────────────────────────────────────────
|
|
183
|
+
|
|
184
|
+
def test_structure_pointer_only_with_a_manifest(g):
|
|
185
|
+
assert g.build_structure_pointer() == []
|
|
186
|
+
(g.PROJECT_ROOT / ".claude").mkdir(exist_ok=True)
|
|
187
|
+
(g.PROJECT_ROOT / ".claude/structure.toml").write_text(
|
|
188
|
+
'mode = "multi-repo"\n\n[[repo]]\nname = "api"\npath = "../api"\npurpose = "Backend"\n')
|
|
189
|
+
text = "\n".join(g.build_structure_pointer())
|
|
190
|
+
assert "`.claude/structure.toml`" in text, text
|
|
191
|
+
if sys.version_info >= (3, 11):
|
|
192
|
+
assert "**api** `../api` — Backend" in text, text
|
|
193
|
+
|
|
194
|
+
|
|
182
195
|
# ── Glossary side-channel (#10) ──────────────────────────────────────────────
|
|
183
196
|
|
|
184
197
|
def test_glossary_json_written_and_shaped(g):
|