scripts/spec-guard.mjs
The coupling guard. It fails a pull request in which a capability's code changed and its spec did not.
node scripts/spec-guard.mjs --base origin/main --block
node scripts/spec-guard.mjs --audit --block
Two modes, and both run on every pull request. The diff mode catches the change in front of you. The audit catches the thing the diff mode structurally cannot: a capability with no entry in capability-map.json is never considered by the diff run, so an unmapped capability is unguarded and silent.
The audit reads the tracked files and the untracked ones git is not ignoring, so a capability directory is in scope from the moment it is created. It used to read the tracked list alone and fall back to walking the filesystem only when git listed nothing at all - which meant one already-tracked spec was enough to hide every new directory, and a local run said OK on the tree CI would fail on as soon as it was staged.
How it decides
It reads specs/capability-map.json, turns each glob into a regular expression (** becomes any path, * stays within a segment), and matches the changed files. A capability whose code matched and whose specs/<capability>/ did not is a failure.
A qualified entry narrows it: {"glob": "config/rules.json", "couples": "shape"} fires on a change to the file's key structure rather than to its values, so editing a number is not a behaviour change while adding a field is. A glob starting with ! excludes, which is how a sibling capability living inside an already-claimed folder gets a coupling of its own instead of both specs being demanded on every edit.
Where the map declares $unclaimed - the paths that belong to no capability by decision - the diff mode also reports a changed file that no capability claims and $unclaimed does not declare. That is the state a map is in when the code went somewhere its globs do not look: every glob matches nothing, and a guard watching an empty set reports OK forever.
{"external": "<repo>", "reason": "..."} says the implementation lives in a repository this one does not own. Nothing is enforced for it - the code is not here to watch - and the audit names it on every run, so a capability cannot leave the guard's reach quietly.
What it cannot catch
A spec edited to say nothing. Touching the file satisfies the guard. That is the known limit of any coupling check, and it is why review still reads the spec diff.
An unmapped capability, in diff mode - hence the audit.
Changes outside the map, in a repo that declares no $unclaimed. Without that declaration the map never claimed to cover everything, so the guard has no basis to call anything unclaimed - and code no capability claims stays unguarded by construction.
The one that surprises people
The guard compares commits, not the working tree. An uncommitted spec fix shows as a failure until it is committed, which reads as a false red the first time and is not one: the pull request is what is being judged.
Decisions behind it
- Per-PR, with no bypass. A spec update riding in a separate pull request makes the guard block the fix, which is the intended pressure: "update the spec before implementing" is the principle, "in the same PR" is what makes it real.
- Globs rather than a build-tool dependency graph. More accurate, and it would tie the guard to one ecosystem. Layer 1 is stack-agnostic by rule.
Reference
- Status. Required - part of the
coreprofile. - How it arrives.
copy- ship verbatim; the repo gets a byte-for-byte copy (guards, templates that carry no repo-specific content) - Shipped form. read it in the standard's own tree
The rule that requires it
Every capability MUST have an entry in
specs/capability-map.json
What checks it
node scripts/spec-guard.mjs --block- domain code changed without touching its spec fails CI Blocks the build when it fails.
