Skip to content

Commit 863c46f

Browse files
pdbethkeclaude
andcommitted
test: a library must not name its consumers' internals
Adds tests/test_vocabulary.py, the same guard now in all four standalone packages. It scans every file `git ls-files` reports -- source, README, pyproject, CI workflows -- for names belonging to a consuming application rather than to this package, and fails the build on a match. A comment convention does not survive a year of commits; a failing test does. This package is clean today, so the guard starts green and stays that way. That is the point of adding it before the code that would trip it. Two decisions worth recording. `\b` is useless here: underscore is a word character, so `\bfoo\b` does not match `foo_driver`, the exact shape this exists to catch; the pattern uses lookarounds treating any non-alphanumeric as a boundary, and the tamper test asserts against that. And `ai` is deliberately absent from the term list -- it appears as an ordinary local (`ai = AttackInput(...)`) in sibling packages, and a guard that cries wolf gets deleted. Same shape as kirby-terrain's leaf test: assert the property, then assert the assertion could actually fail. A third test checks `git ls-files` returned something, since an empty scan would make the whole thing vacuous. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016bW75jKdZ6MrHipXbw7ZRw
1 parent bc0611c commit 863c46f

1 file changed

Lines changed: 121 additions & 0 deletions

File tree

tests/test_vocabulary.py

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
"""A library does not name its consumers' internals.
2+
3+
kirby-combat, kirby-cost, kirby-dice and kirby-terrain are standalone
4+
packages. A comment here that names a module in a consuming application --
5+
or worse, cites line numbers inside it -- is a leaky abstraction and a
6+
guaranteed source of rot: those offsets were pinned against a file in
7+
another repository that no CI here can check, and they were wrong within
8+
weeks. Describe the ROLE ("the consumer's driver", "the chooser") and the
9+
comment stays true for as long as the role does.
10+
11+
The same rule covers vendor and product names. This package models
12+
CHOOSING -- a `chooser`, an `option`, a `situation`, a `decide()` -- and
13+
that vocabulary is deliberately neutral about what implements the choice.
14+
The neutrality is the better design as well as the tidier one: a scripted
15+
chooser and any other kind are then indistinguishable to the engine, which
16+
is exactly what lets a fight run with no dependencies at all.
17+
18+
A comment convention does not survive contact with a year of commits. This
19+
does, because it fails the build. It follows the same shape as
20+
kirby-terrain's leaf test: assert the property, then assert the assertion
21+
could actually fail.
22+
23+
To permit a genuine exception, add it to ALLOW with a reason. Deliberate
24+
and reviewable beats a silently widened pattern.
25+
"""
26+
import pathlib
27+
import re
28+
import subprocess
29+
30+
#: Unambiguous vendor, product and technique names. Deliberately NOT "ai":
31+
#: `ai = AttackInput(...)` is an ordinary local in several tests, so banning
32+
#: it would train everyone to ignore this test -- the failure mode that ends
33+
#: with the guard deleted.
34+
TERMS = [
35+
"llm", "ollama", "anthropic", "openai", "gpt", "chatgpt", "claude",
36+
"gemini", "mistral", "llama", "huggingface", "copilot", "bedrock",
37+
"medialib", "prompt",
38+
]
39+
40+
#: Underscore is a word character, so `\bllm\b` does NOT match `llm_driver` --
41+
#: the exact string this test exists to catch. These lookarounds treat any
42+
#: non-alphanumeric as a boundary, so `llm_driver`, `LLM-driver` and `(llm)`
43+
#: all match.
44+
PATTERN = re.compile(
45+
"|".join(rf"(?<![a-z0-9]){t}(?![a-z0-9])" for t in TERMS), re.IGNORECASE
46+
)
47+
48+
ROOT = pathlib.Path(__file__).resolve().parent.parent
49+
50+
#: (path, matched term) -> why it is allowed to stay.
51+
ALLOW: dict[tuple[str, str], str] = {
52+
(".gitignore", "claude"): "editor tooling directory, not a project reference",
53+
}
54+
55+
56+
def _tracked_files() -> list[str]:
57+
"""Every file git tracks -- source, README, pyproject, CI workflows.
58+
59+
Uses git rather than rglob so the scan covers exactly what is published
60+
and nothing that is not: no .venv, no build artefacts, no local scratch.
61+
"""
62+
out = subprocess.run(
63+
["git", "-C", str(ROOT), "ls-files"],
64+
capture_output=True, text=True, check=True,
65+
)
66+
return out.stdout.split()
67+
68+
69+
def _offenders() -> list[str]:
70+
found: list[str] = []
71+
here = pathlib.Path(__file__).name
72+
for rel in _tracked_files():
73+
# This file necessarily contains every banned term.
74+
if pathlib.Path(rel).name == here:
75+
continue
76+
path = ROOT / rel
77+
try:
78+
text = path.read_text(encoding="utf-8")
79+
except (UnicodeDecodeError, OSError):
80+
continue # binary or unreadable; nothing to read anyway
81+
for lineno, line in enumerate(text.splitlines(), 1):
82+
for match in PATTERN.finditer(line):
83+
term = match.group(0).lower()
84+
if (rel, term) in ALLOW:
85+
continue
86+
found.append(f"{rel}:{lineno}: {term!r} in {line.strip()[:80]!r}")
87+
return found
88+
89+
90+
def test_the_published_source_names_no_inference_vocabulary() -> None:
91+
offenders = _offenders()
92+
assert offenders == [], (
93+
"A library must not name its consumers' internals. These "
94+
"references do:\n "
95+
+ "\n ".join(offenders)
96+
+ "\n\nRephrase to describe the ROLE ('the consumer's driver', 'the "
97+
"chooser') rather than the implementation. If a reference is "
98+
"genuinely necessary, add it to ALLOW with a reason."
99+
)
100+
101+
102+
def test_the_guard_would_notice_a_leak() -> None:
103+
"""Guards the guard.
104+
105+
A word-boundary bug would make the test above pass on every input while
106+
appearing to work -- so assert against the specific string that motivated
107+
it, underscore and all.
108+
"""
109+
assert PATTERN.search("see llm_driver.py for details")
110+
assert PATTERN.search("calls the Anthropic API")
111+
assert PATTERN.search("OLLAMA_BASE_URL")
112+
# ...and does not fire on ordinary prose or an `ai` local.
113+
assert not PATTERN.search("ai = AttackInput(attacker=a, target=b)")
114+
assert not PATTERN.search("the domain contains available terrain")
115+
116+
117+
def test_the_scan_actually_reads_files() -> None:
118+
"""A `git ls-files` that returned nothing would make the guard vacuous."""
119+
tracked = _tracked_files()
120+
assert len(tracked) > 5, tracked
121+
assert any(f.endswith(".py") for f in tracked)

0 commit comments

Comments
 (0)