diff --git a/scripts/CLAUDE.md b/scripts/CLAUDE.md index 399a5fb5..4db3be64 100644 --- a/scripts/CLAUDE.md +++ b/scripts/CLAUDE.md @@ -62,10 +62,11 @@ imports it, except `mcp-app`, which no widget imports and which lands in `skills/leaf/mcp-app/` for an MCP host to read from the install. A bundle reproduces its tracked bytes exactly when every input it fetches is pinned, -which holds for `marked`, `sortable`, `beautiful-mermaid`, `highlight`, and `jsdiff`, -so a clean `git status` after a run is the check that the bundle still matches the -script. `plot` and `pierre` reach npm's resolver for transitive dependencies and inherit -its ranges, so a diff from either can be an upstream patch rather than drift. +which holds for `marked`, `sortable`, `beautiful-mermaid`, `floating-ui`, `highlight`, +and `jsdiff`, so a clean `git status` after a run is the check that the bundle still +matches the script. `plot`, `pierre`, and `mcp-app` reach npm's resolver for transitive +dependencies and inherit its ranges, so a diff from one of those can be an upstream +patch rather than drift. Rerun a bundle after changing its pin or the registry input it reads; do not patch a generated bundle or `examples/corpus.html` directly. diff --git a/tests/test_interact_layer.py b/tests/test_interact_layer.py index f403e1bd..5facdaa9 100644 --- a/tests/test_interact_layer.py +++ b/tests/test_interact_layer.py @@ -1,5 +1,6 @@ """CLI, plugin payload, layer, and customization tests.""" +import ast import contextlib import json import os @@ -299,6 +300,51 @@ def names(scope): assert not unnamed, f"unnamed in scripts/CLAUDE.md: {unnamed}" +def test_the_tooling_instructions_place_every_vendored_bundle(): + """Whether a rebuild reproduces its bytes must be named where sessions read. + + `scripts/CLAUDE.md` says a clean `git status` after `vendor.py ` is the + check that the bundle still matches the script, and that the check holds only for + the bundles whose every fetched input is pinned. That sentence is what a session + consults before reading a rebuild's diff as drift or as an upstream patch, so a + bundle it never places has no answer either way — which is how `floating-ui`, whose + pins cover its whole closure, and `mcp-app`, whose do not, both went unplaced. The + names come from `vendor.py` rather than a list here, for the reason the routing + above states: a list is the second copy, and the bundle added without the sentence + would stay green. + """ + tree = ast.parse((ROOT / "scripts" / "vendor.py").read_text(encoding="utf-8")) + bundles = { + key.value + for node in ast.walk(tree) + for target in ( + node.targets + if isinstance(node, ast.Assign) + else [node.target] + if isinstance(node, ast.AnnAssign) + else [] + ) + if isinstance(target, ast.Name) and target.id in {"BUILDS", "COPIES"} + for key in node.value.keys + if isinstance(key, ast.Constant) + } + paragraphs = [ + paragraph + for paragraph in (ROOT / "scripts" / "CLAUDE.md") + .read_text(encoding="utf-8") + .split("\n\n") + if paragraph.startswith("A bundle reproduces its tracked bytes exactly") + ] + + assert bundles, "no bundles read — an empty set places itself" + assert len(paragraphs) == 1, ( + "scripts/CLAUDE.md no longer opens one paragraph with " + f"'A bundle reproduces its tracked bytes exactly': {len(paragraphs)} found" + ) + unplaced = sorted(name for name in bundles if f"`{name}`" not in paragraphs[0]) + assert not unplaced, f"unplaced in scripts/CLAUDE.md: {unplaced}" + + def test_the_root_instructions_name_every_directory_ci_gates_on_its_own(): """A gate `uv run pytest tests` does not reach must be named where sessions read.