// tools/guardrails.mjs - lines 67-92
const gateRule = manifest.gateTargets
if (gateRule) {
const pkg = JSON.parse(readFileSync(join(root, "package.json"), "utf8"))
const gate = pkg.scripts?.[gateRule.script] ?? ""
const match = /nx\s+affected\s+-t\s+([^\s&|]+)/.exec(gate)
const targets = match ? match[1].split(",").map((t) => t.trim()) : []
if (targets.length === 0) {
problems.push(
`the ${gateRule.script} script does not derive its targets from the project graph`
)
}
for (const target of gateRule.required ?? []) {
if (!targets.includes(target)) {
problems.push(
`the ${gateRule.script} script does not run "${target}" (${gateRule.why ?? ""})`.trim()
)
}
}
for (const target of gateRule.forbidden ?? []) {
if (targets.includes(target)) {
problems.push(
`the ${gateRule.script} script runs "${target}", which the repository does not use (${gateRule.why ?? ""})`.trim()
)
}
}
}
The targets are parsed out of the gate script, so they come from the project
graph rather than from a list somebody edits. The history of this field is why
it has a direction at all:
$ git show 8c75684 -- tools/guardrails.manifest.json # gateTargets introduced
…
+ "forbidden": ["test", "test-storybook"],
…
$ git show b200e23 -- tools/guardrails.manifest.json # the flip
- "forbidden": ["test", "test-storybook"],
…
+ "required": ["test"],
+ "forbidden": ["test-storybook"],
A rule that can only ever say no has to be rewritten when the decision
changes. A rule with a direction survives the decision changing: test moved
from forbidden to required, and the manifest moved with it.
That test target is the one F1 removed. It came back on 2026-11-03, and the
gate that runs it today also runs the guardrails first:
$ git show b200e23:package.json | grep '"gate"'
"gate": "bun run budget && bun run guardrails && nx affected -t lint,format,typecheck,build,build-storybook,test"
4. Separately bounded budgets
// tools/size-budget.mjs - lines 11-14
const MAX_FILES = 95
const MAX_LINES = 3300
const MAX_TEST_FILES = 12
const MAX_TEST_LINES = 400
$ bun run budget
size budget: 81/95 files, 3073/3300 non-blank lines
test budget: 4/12 files, 227/400 non-blank lines
The product budget and the test budget are counted separately, and the split
is the point. If tests counted against the product budget, the first test
would cost a line of product code, and the budget would push toward removing
verification. Excluding tests without bounding them would make the exclusion
a hole. Bounding them separately makes it a rule.
All four, running
$ bun run guardrails
Recorded removals:
HARNESS.md — removed with the test harness in #43 — the spec-first scaffold the harness owned
docs/specs/ — removed with the test harness in #43 — the spec-and-test loop; a unit test loop came back under 0019, without the specs
docs/decisions/0005-sdd-and-tdd.md — removed with the test harness in #43 — it recorded the spec-and-test rules that went with it
docs/decisions/0007-storybook-scope.md — removed with the test harness in #43 — its per-library Storybook and story-per-component rules are still in force
apps/web/AGENTS.md, libs/shared/ui/AGENTS.md, libs/web/features/*/AGENTS.md — per-project agent instructions removed with the test harness in #43; the root AGENTS.md is the instruction file now
guardrails: 9 files, 6 invariants, 17 records, 165 tracked files scanned — all present
Both scripts are Node built-ins only and run from the repository root, a
worktree or CI. The guardrails script also runs as its own job, Guardrails,
and inside the gate.
The rest of it
Four more checks had to become mechanical before the first four meant anything.
Recorded removals
Five entries, each naming what was removed and why, printed on every run:
// tools/guardrails.mjs - lines 219-224
const removed = Object.entries(manifest.removed ?? {})
if (removed.length > 0) {
console.log("Recorded removals:")
for (const [path, reason] of removed) console.log(` ${path} — ${reason}`)
console.log("")
}
// tools/guardrails.manifest.json - lines 147-149
"removed": {
"HARNESS.md": "removed with the test harness in #43 — the spec-first scaffold the harness owned",
"docs/specs/": "removed with the test harness in #43 — the spec-and-test loop; a unit test loop came back under 0019, without the specs",
This is decision record 0009's principle, made mechanical: "An unexplained
absence reads as an oversight. A recorded exclusion reads as judgement."
(docs/decisions/0009-not-built.md, Context). The repository now prints its
own absences, with reasons, every time the guardrails run.
Forbidden paths
Fourteen globs, matched against every tracked file at any depth:
// tools/guardrails.mjs - lines 123-131
for (const path of tracked) {
for (const rule of forbidden) {
// Matched against the whole path and against the file name, so a pattern
// like "*-original" means any depth rather than only the repository root.
if (rule.matcher.test(path) || rule.matcher.test(basename(path))) {
problems.push(`tracked file matches a forbidden path pattern: ${path} (${rule.pattern})`)
}
}
}
The comment in that loop is the fix for one of the three wrong-reason checks
in the section above: the earlier version matched the repository root only, so
a nested forbidden path was never matched.
Secret patterns
Eight regular expressions (GitHub, Chromatic, Nx Cloud, AWS, private keys)
applied to every tracked text file under a megabyte:
// tools/guardrails.mjs - lines 160-172
for (const path of tracked) {
if (secretPatterns.length === 0) break
if (BINARY_EXTENSIONS.has(extname(path).toLowerCase())) continue
const full = join(root, path)
if (!existsSync(full)) continue
if (statSync(full).size > 1024 * 1024) continue
const text = readFileSync(full, "utf8")
for (const rule of secretPatterns) {
if (rule.matcher.test(text)) {
problems.push(`tracked file contains something shaped like a secret: ${path} (${rule.name})`)
}
}
}
Project tags
Tags come from a closed vocabulary: a scope (shared, web)
and a type (ui, feature, app). A project with no tags fails, because the
boundary rule matches on tags and a project with no tags has no boundary:
// tools/guardrails.mjs - lines 205-209
const tags = isProjectFile ? json.tags : json.nx?.tags
if (!Array.isArray(tags) || tags.length === 0) {
problems.push(`project has no tags: ${path}`)
continue
}
The distinction that keeps the secret check honest
A pattern check is cheap and mechanical, and it covers the shape of a
credential. It did not find the credential that was on the machine. That was
found by a separate search, run by hand, for the exact values of the credentials
held in local files. The corpus says so itself:
Top comments (0)