/prevent-regression
Swift 6.3.3 exact-property-type guard
Use scripts/stage_swift_state_guard.py only after a fresh accepted Swift enum
proposal and a second content-addressed acceptance of the migrated tree and
exact buildable String reversion. Keep sibling _swift-semantic-readonly. The
skill stages but never installs one same-module compile-time property-type
guard derived from the accepted owner, member, and existing enum identity. It
proves the accepted migrated tree passes build/check/smoke and guard typecheck,
then proves the accepted reversion still passes native gates without the guard
but fails with it. Case/raw mappings remain proposal evidence rather than a
guard claim; runtime behavior, Codable/reflection, dynamic/protocol dispatch,
frameworks, generated inputs, external variants, and compatibility remain
outside the guard.
C# staged exact-property-type guard
Use scripts/stage_csharp_state_guard.py only after a fresh accepted C# enum
migration and current sibling _csharp-semantic facts. Read
knowledge/csharp-v1.md for the exact command. The skill stages but never
installs one exact property-type compile guard and proves a disposable string
reversion fails. Wire values, runtime behavior, other properties, generated
code, frameworks, external callers, and binary compatibility remain outside
the claim.
Kotlin/JVM 2.4.10 exact-type guard
Trigger this branch only from a fresh accepted Kotlin enum proposal and a
second content-addressed migration acceptance. Keep sibling
_kotlin-semantic, read
../_kotlin-semantic/GUIDE.md, and enter through
scripts/stage_kotlin_state_guard.py. It stages but never installs one
compile-time exact-property-type assertion and proves an accepted buildable
String reversion fails with the guard. It does not protect wire values,
reflection, delegation, generated/plugin sources, Gradle variants,
Java/framework/external callers, runtime behavior, JVM ABI, or general Kotlin
lint coverage.
C++20 branch
Use scripts/stage_cpp_state_guard.py with a fresh accepted migration and all
five human approvals; run it with --help for the exact CLI. It stages, but
never installs, one std::is_same_v guard for an exact namespace-qualified
field and proves one disposable type regression fails. It makes no broader
ODR/ABI, specialization, overload/dispatch, external-variant, or runtime claim.
C17 branch
Use scripts/generate_c_state_guard.py on an accepted C enum proposal, then
verify the staged bundle with scripts/verify_c_state_guard.py; run each with
--help for the exact CLI. This branch stages but never installs one exact-
field _Static_assert/_Generic type guard; values, assignments, aliases,
runtime behavior, other fields, and general lint coverage remain unresolved.
PHP and Ruby
For an accepted PHP enum proposal, read _php-proposal/GUIDE.md. For an
accepted Ruby RBS proposal, read _ruby-semantic/PROPOSAL-GUIDE.md. Both
branches stage and verify one exact reviewed field guard; neither installs it.
Dart exact-field guard
After a human accepts the Dart enum proposal, stage—not install—a dependency-free project-owned guard for that exact reviewed public field. The verifier proves a good disposable tree passes and a buildable String reversion fails specifically because of the staged guard.
SKILL_ROOT=".agents/skills/on-demand/prevent-regression"
python3 "${SKILL_ROOT}/scripts/generate_dart_state_guard.py" \
--project-root "$PWD" --evidence-dir reports/implicit-state/dart \
--acceptance reports/implicit-state/dart/acceptance.json \
--targets reports/extract-enum/dart/targets.json \
--accepted-review reports/extract-enum/dart/accepted-review.json \
--output-root "$PWD/reports/prevent-regression/dart-state"
python3 "${SKILL_ROOT}/scripts/verify_dart_state_guard.py" \
--project-root "$PWD" --stage reports/prevent-regression/dart-state
This is one field guard, not a universal lint or a runtime/serialization invariant. Audited host source is never modified.
Rust exact-field guard
After a human accepts an extract-enum proposal, require a SHA-bound
rust-enum-review-v1 acceptance artifact. Stage—not install—a project-owned
Cargo integration test that asserts one public owner/public field has the
reviewed enum type, then verify it with locked/offline metadata, check, test,
Clippy, and rustfmt. Private fields abstain; this is not a general Rust lint.
SKILL_ROOT=".agents/skills/on-demand/prevent-regression"
OUT="$PWD/reports/prevent-regression/rust-state"
python3 "${SKILL_ROOT}/scripts/generate_rust_state_guard.py" \
--targets "$PWD/reports/extract-enum/rust/targets.json" \
--accepted-review "$PWD/reports/extract-enum/rust/accepted-review.json" \
--project-root "$PWD" --output-root "$OUT"
python3 "${SKILL_ROOT}/scripts/verify_rust_state_guard.py" \
--stage "$OUT" --project-root "$PWD" \
--output "$OUT/verification.json"
You are the orchestrator for turning a closed cleanup into a permanent
guardrail. Invocation only stages a proposal below
reports/prevent-regression/<id>/; it never authorizes rollout. Stage rule,
fixture, and ready-to-apply wiring at their repo-relative destination paths,
then let the human install or discard them.
How success is judged
- Emit, never install, the artifact and verification recipe.
- Paste the real verifier result: BAD_RC=1 and GOOD_RC=0 for lint guards, or focused test output for behavioral guards.
- Prove the historical fire and current clean result; fixtures cover every matched anti-pattern plus legitimate forms.
- Use
_common/skill-conventions.md,agents/rule-designer.md, and the bundled generation/verifier helpers for the shared report shape.
Closed-state guard
For the reviewed first-party status / phase / state path, use the
bundled guard directly after installation:
python3 scripts/stringly_status_guard.py <file-or-directory>
It blocks a bare string Django field, comparison, or assignment and accepts
only a reasoned # noqa: stringly-status: <reason> vendor boundary. This is a
family-local reference guard; it does not generate or install unrelated
general-purpose rules.
TypeScript closed-state guard
Stage—not install—the family-local Compiler API guard for a reviewed closed
TypeScript field. It covers direct/reversed comparisons, one-hop const aliases,
plain/??=/chained assignments, and transparent parentheses. A pinned host
TypeScript package plus tsconfig are required; unsupported general dataflow,
ORM behavior, and root lint wiring remain out of scope.
ID="typescript-state"
OUT="reports/prevent-regression/$ID"
node .claude/skills/prevent-regression/scripts/generate_typescript_state_guard.mjs \
--id "$ID" \
--project-root "$(pwd)" \
--tsconfig "$(pwd)/tsconfig.json" \
--output-root "$OUT"
node .claude/skills/prevent-regression/scripts/verify_typescript_state_guard.mjs \
--rule "$OUT/scripts/lint/no_stringly_state.mjs" \
--bad "$OUT/tests/lint/no_stringly_state_bad.ts" \
--bad-tsx "$OUT/tests/lint/no_stringly_state_bad.tsx" \
--good "$OUT/tests/lint/no_stringly_state_good.ts" \
--good-tsx "$OUT/tests/lint/no_stringly_state_good.tsx"
The report stages guard, TS/TSX fixture pairs, and host-wiring.diff; paste
BAD_RC=1/GOOD_RC=0. A reasoned noqa only suppresses a Compiler-resolved vendor
receiver, never a filename or forged first-party comment. The serial integrator
owns staged wiring.
Checked-JavaScript closed-state guard
Retain complete checked-JavaScript detector evidence and manifest first. The
generator accepts only at least one proven first-party operation from an
allowJs + checkJs host Compiler API run; partial/unsupported evidence exits
2 rather than fabricating findings.
ID="javascript-state"
OUT="reports/prevent-regression/$ID"
node .claude/skills/prevent-regression/scripts/generate_javascript_state_guard.mjs \
--id "$ID" \
--project-root "$(pwd)" \
--config "$(pwd)/jsconfig.json" \
--findings reports/implicit-state/findings.jsonl \
--manifest reports/implicit-state/manifest.json \
--output-root "$OUT"
node .claude/skills/prevent-regression/scripts/verify_javascript_state_guard.mjs \
--rule "$OUT/scripts/lint/no_stringly_state_javascript.mjs" \
--bad "$OUT/tests/lint/no_stringly_state_bad.js" \
--bad "$OUT/tests/lint/no_stringly_state_bad.jsx" \
--bad "$OUT/tests/lint/no_stringly_state_bad.mjs" \
--bad "$OUT/tests/lint/no_stringly_state_bad.cjs" \
--good "$OUT/tests/lint/no_stringly_state_good.js" \
--good "$OUT/tests/lint/no_stringly_state_good.jsx" \
--good "$OUT/tests/lint/no_stringly_state_good.mjs" \
--good "$OUT/tests/lint/no_stringly_state_good.cjs"
The proposal stages four fixture pairs and wiring; paste BAD_RC=1/GOOD_RC=0. Coverage is direct/reversed comparison, assignment, and one-hop const aliases on JSDoc-closed fields; vendor noqa requires Compiler API evidence. The serial integrator owns installation.
Go closed-state guard
After a human accepts a Go proposal, read and follow knowledge/go-state.md.
Load that file only for Go work. Never weaken an unavailable semantic closure
to a field-name regex.
Java 17 exact-authority guard
After a human accepts a Java enum proposal, stage—not install—a guard from its
targets.json. It copies the accepted qualified owner, field, source
fingerprint, and family-local JDK compiler helper. The staged rule checks only
that compiler-resolved direct String field; it does not broadly match every
field named state, status, or phase. It catches bare assignments,
String.equals/Objects.equals, and unsafe ==/!= while that exact field
is still String.
OUT="${OUT:-reports/prevent-regression/java-job-status}"
TARGETS="${TARGETS:-reports/extract-enum/java-job-status/targets.json}"
SKILL_ROOT=""
for SKILL_CANDIDATE in \
".agents/skills/on-demand/prevent-regression" \
".agents/skills/prevent-regression" \
".claude/skills/prevent-regression"
do
if [ -f "${SKILL_CANDIDATE}/SKILL.md" ]; then
SKILL_ROOT="$(cd "${SKILL_CANDIDATE}" && pwd)"
break
fi
done
if [ -z "${SKILL_ROOT}" ]; then
printf '%s\n' "prevent-regression is not installed in .agents/skills/on-demand, .agents/skills, or .claude/skills" >&2
exit 2
fi
python3 "${SKILL_ROOT}/scripts/generate_java_state_guard.py" \
--targets "$TARGETS" \
--project-root "$(pwd)" --output-root "$OUT"
RULE="$(find "$OUT/scripts/lint" -maxdepth 1 -name 'no_stringly_state.py' -print -quit)"
python3 "${SKILL_ROOT}/scripts/verify_java_state_guard.py" \
--rule "$RULE" --authority "$OUT/authority.json" \
--bad "$OUT/tests/lint/bad/Job.java" --good "$OUT/tests/lint/good/Job.java" \
--project-root "$(pwd)"
Install find-implicit-state alongside this skill so the generator can copy
its helper; a missing helper or JDK 17 exits 2 rather than falling back to a
name regex. The verifier pastes BAD_RC=1, GOOD_RC=0 and compiles both fixtures
with javac --release 17 -proc:none. This branch modifies only the staged
proposal directory, never host guard files; a human integrator owns any later
installation, native check, and rollback.
Argument parsing
Four forms. Detect and route:
Form A — Cluster ID from a recent scan
Pattern: cluster:<id>, delete:<id>, fix:<id>, semantic:<id>,
dormant:<id> — anything /fix-workflow accepts.
Load the corresponding triage entry from
reports/<scan>/latest/report.md or triage.md and extract:
- files touched
- fix shape (e.g.
quasi_dead_broken,extract_helper) - the git commit(s) that landed the fix (from the learnings log or
git log --grep=<id>)
Form B — Explicit pattern description
A sentence plus an optional path:<regex> qualifier. Example:
"bare
int(request.POST.get(...))withoutsafe_intin views"path:core/views/**/*.py
Treat the sentence as the rule intent. Before producing a proposal, ask for one clarification round if the path scope or anti-pattern AST shape is ambiguous.
Form C — --dogfood <rule-name>
Used when the rule script already exists under scripts/lint/ and
you only need to (re)wire it, regenerate fixtures, or refresh the
CLAUDE.md entry. Skip Phase 1 and start at Phase 2.
Form D — Product-topology guard template
Pattern: topology:<route-ownership|workflow-registry|frontend-boot|site-endpoints|doc-route-drift>.
Use this when a workflow cleanup has already landed and the user wants to prevent drift from returning. The skill should start from the corresponding product-topology report, not from a raw grep:
route-ownership: block new workflow routes from being added to the global root URL file once an include boundary exists.workflow-registry: block new sidebar/dashboard step definitions outside the canonical registry.frontend-boot: block new template-levelwindow.*globals outside the canonical boot payload.site-endpoints: block hardcoded registry-owned/api/sites/<site_id>/...consumer strings once the workflow boot payload exposes endpoint keys.doc-route-drift: add a scheduled or diff-scoped docs-vs-routes check when route docs repeatedly drift.
Core beliefs
- Ruff first. Use a custom rule only where Ruff is silent or too broad.
- Diff-scope and reasoned escapes. Hooks inspect changed files; custom noqa markers require a non-empty reason.
- Fixture truth. Every matcher branch fires in bad fixtures and stays quiet in good fixtures; the verifier is the proof.
- One invariant, one commit. Bundle rule/test, fixtures, verifier, and staged wiring so a revert and bisect remain meaningful.
- Choose the right peer. Use lint for local shape, tests for behavioral contracts, and audit bands only when neighbor/path context is essential.
- Protect an accepted convention. Derive registry facts where possible; do not guard topology before its canonical shape exists.
New-Feature Guardrail Prompts
When a feature establishes a reusable convention, ask whether a lightweight guard belongs with it: reverse-backed route tests before endpoint lints; one boot payload plus a retired-global check; typed state authority from creation; a dormant sweep or quarantine for prototype remnants; endpoint/credential tests before bespoke lints; and broker-failure cleanup tests for safe dispatch.
Phase 1 — Pattern discovery
Pre: argument resolved.
Post: reports/prevent-regression/<id>/pattern.md exists with
the rule intent, path scope, file suffixes, and match shape.
Run git show <commit> for the fix commit(s). Read the diff. Classify:
- Ruff-coverable: the pattern corresponds to an existing ruff rule
(consult https://docs.astral.sh/ruff/rules/). Emit a
pyproject.tomldiff that enables the rule and optionally narrowsper-file-ignores. Skip to Phase 4. - Custom rule: the pattern is narrower, domain-specific, spans a non-Python language, or requires looking at source comments (e.g. the allow-list shape). Proceed to Phase 2.
Record the classification, rule name, path scope, file suffixes, and
anti-pattern shape. For Python, describe the AST shape (what
ast.walk looks for). For JS/templates/docs, describe the lexical or
parser shape and the false-positive boundaries. Use symbolic names in
prose — see _common/skill-conventions.md "No raw line numbers".
Phase 2 — Rule script
Pre: classified as custom rule.
Post: scripts/lint/<rule>.py exists and is smoke-tested.
Contract for every custom rule:
- Stdlib-only. Runs under bare
python3(no.venvrequired). - CLI: accepts
<file>/<directory>positionals OR--stdin --filename=<name>; exit 0 on clean, 1 on violations, 2 on invocation error. Directory scans usescripts/lint/path_utils.pyfor skip directories and expand only the rule's suffixes. - Output: one line per violation, format
<path>:<line>:<col>: <rule>: <message>. - Allow-list:
<comment> noqa: <rule>: <reason>on any line of the matched span, reason regex\S(non-empty). Use#for Python and//for JS.
silent_catch.py is the Python reference implementation. For JS
lexical guards (no_site_endpoint_sprawl.py was the source host's
exemplar — not shipped in this ecosystem;
keep this shape: suffix expansion, template-literal/string-concat
matching, blockable comments, and a reason-required // noqa.
Phase 3 — Fixtures
Pre: rule script works on synthetic input.
Post: tests/lint/<rule>_bad.<ext> + tests/lint/<rule>_good.<ext>
exist and verify_rule.py passes.
<rule>_bad.<ext> must contain every variant of the anti-pattern the
rule is supposed to catch — at minimum one example per branch of the
rule's matcher. <rule>_good.<ext> must contain the legitimate
patterns the rule must NOT flag: narrow exceptions, logged paths,
re-raises, allow-listed cases.
Python fixtures carry # ruff: noqa at the top so they don't
double-fail the standard ruff pass. Non-Python fixtures use the normal
comment style for explanatory fixture comments.
Run:
python3 .claude/skills/prevent-regression/scripts/verify_rule.py \
--rule scripts/lint/<rule>.py \
--bad tests/lint/<rule>_bad.<ext> \
--good tests/lint/<rule>_good.<ext>
It must report BAD_RC=1 (violations fired) and GOOD_RC=0 (no false positives). If either is wrong, iterate on the rule or fixtures.
Phase 3b — Test-only guard
Use this when a focused test expresses the invariant better than a lint: import-path stability, retired files, endpoint/auth/CSRF contracts, credential absence, removed fake fallbacks, or dispatch cleanup. Write the smallest test, derive canonical routes/keys where practical, run it plus baseline, and retain the proposal's pattern, verification, and follow-on sections.
Phase 4 — Wire into pre-commit + CI
Pre: verify_rule.py passes.
Post: .pre-commit-config.yaml has a local hook entry for the
rule; .github/workflows/ci.yml has a diff-scoped step that runs the
rule on the same file set.
Hook shape. Keep pre-commit broad and put the real rule scope in
scripts/lint/run.py so local hooks and CI share the same filter:
- repo: local
hooks:
- id: <rule>
name: "<rule> (<short rationale>)"
entry: python3 scripts/lint/run.py --rule <rule>
language: system
types: [python]
files: '^core/.*\.py$'
For non-Python guards, set types and files to the broad staged-file
surface for that language, and add a RuleSpec.suffixes entry in
scripts/lint/run.py so --staged, --changed-from, and explicit
paths all collect the same file types.
CI runs python3 scripts/lint/run.py --changed-from "$BASE" --rule all.
When adding a rule, add one RuleSpec in the runner; do not duplicate
the scope regex in workflow shell.
Phase 5 — Canonical Patterns entry
Pre: rule wired.
Post: .claude/CLAUDE.md has a new bullet under "Canonical
Patterns" naming the rule, its intent, and the allow-list shape.
Entry template (one bullet, ≤4 lines):
- **`<rule>` / `scripts/lint/<rule>.py`** — <one-line intent>. The
allow-list is `<comment> noqa: <rule>: <reason>` (reason required).
See `tests/lint/<rule>_{bad,good}.<ext>` for the canonical examples.
Match the existing "Canonical Patterns" list's voice — imperative, no emoji.
Phase 6 — Proposal + report
Pre: Phases 1–5 complete.
Post: reports/prevent-regression/<id>/proposal.md written,
reports/_meta/effectiveness.jsonl appended, user notified.
proposal.md structure:
# Rule proposal: <rule>
## Source cluster
<cluster id + commit ref>
## Pattern
<match shape + path scope + rationale>
## Artifacts
- lint guard: scripts/lint/<rule>.py (new),
tests/lint/<rule>_{bad,good}.<ext> (new),
.pre-commit-config.yaml (modified),
.github/workflows/ci.yml (modified),
.claude/CLAUDE.md (modified — Canonical Patterns entry)
- test-only guard: focused regression test module(s) plus any
Canonical Patterns / testing-doc entry needed to explain the
invariant
## Verification
- `verify_rule.py`: BAD_RC=1 with <N> hits, GOOD_RC=0.
- Historical regression: rule fires on
`git show <anchor>^:<file>` for each pre-fix site.
- Clean on current HEAD: <list files checked>.
## Follow-on findings
<violations surfaced on other in-scope files that this cluster
did NOT fix — these become new /fix-workflow or /find-dormant
candidates, not Phase 1 work>
Append to reports/_meta/effectiveness.jsonl using the shared
scripts/log_effectiveness.py:
python3 scripts/log_effectiveness.py \
--skill prevent-regression \
--scan-id "prevent-regression-${TS}" \
--target "scripts/lint/<rule>.py" \
--findings-total N \
--buckets '{"<rule>": N}' \
--notes "<optional>"
Step 7 — Summarize
Report to the user in ≤10 lines:
- Rule name + AST shape.
- Files created/modified (1 line each).
- Verification results (pass/fail).
- Follow-on findings count (if any).
- Path to
proposal.md. - Next recommended action: human reviews the proposal, installs the staged artifacts and wiring diffs, and commits — or abort if verification failed.
Non-goals
- Fixing pre-existing violations on unrelated files — that's
/fix-workflowwork, surfaced here as follow-on findings. - Broadening the rule's AST shape beyond what the source cluster demonstrated — two clusters justify one rule, not a family.
- Running full-repo lint — the rule is diff-scoped by design.
Repository layout
.claude/skills/prevent-regression/
├── SKILL.md # this file — orchestrator
├── agents/
│ └── rule-designer.md # pattern-shape scout brief
├── knowledge/ # host-overlay slot — ships empty;
│ # conventions live in _common/skill-conventions.md
└── scripts/
├── generate_rule.py # rule scaffold generator (Phase 2)
└── verify_rule.py # fixture-pair verifier (Phase 3)