Files
suaveplan/GENERIC-OPENSPEC-AGENTS.md
2026-05-27 23:29:53 +01:00

28 KiB
Raw Permalink Blame History

OpenSpec — Generic Agent Compliance Checklist

A reusable spec-driven change-management workflow for any project. Read this file before implementing any OpenSpec change. Every section is a non-negotiable gate. Paired with GENERIC-CLAUDE.md for general engineering standards.


0. Research before implementing

Before writing a new package or migrating a library — mandatory, not optional:

  1. Search the registry (npm / PyPI / crates.io / etc.) for existing implementations. Prefer battle-tested over hand-rolled. Check the root manifest's catalog / dependencies block for deps already pinned.
  2. Check vendor docs (via Context7 or similar) to confirm API behavior and version-specific details before wrapping an external library.
  3. Search your own repo for reference implementations and patterns. Try local copies of upstream sources first.
  4. If an existing implementation covers 80%+ of the requirement, port or wrap it — do not start from scratch.
  5. If a starting-from-scratch is unavoidable but a prior implementation exists, read its nested .md files first to understand its purpose.
  6. Check the dep graph — if a new type or abstraction is shared across ≥2 packages, it belongs in a zero-dep shared types package. Add it there first, then reference it. Prevents circular dependencies and keeps the public contract in one place.

0a. Migration parity — no cut-down replacements

When migrating a library from an upstream source, a greenfield rewrite is a last resort, not a default. The upstream library exists because it solved real problems. Dropping features because they are "complex" or "not needed yet" is how you end up with broken consumers later.

Mandatory parity checklist

Every migration proposal MUST include a ## Parity Checklist section. Before any code, read the upstream source, any upstream specs, and any upstream audit docs. Then produce a table:

| Capability | Upstream | Migrated | Notes |
|---|---|---|---|
| Feature A | ✅ | ✅ | |
| Feature B | ✅ | 🚫 | Scoped out: reason … |
| Feature C | ✅ | ⬆️ | Enhanced: added … |

Rules:

  • No implicit drops. If upstream has it and the migration does not, the box is 🚫 and MUST have a justification.
  • "Not needed yet" is banned as a justification. Scope out only with a concrete architectural reason (e.g. "replaced by interceptor API which covers the same use case via composition").
  • If the justification is longer than one sentence, the feature is not scoped out — it is required.
  • The checklist is a gate. A proposal with unchecked 🚫 items without justification is rejected.

Design quality bar

Migrations must be modular and extensible:

  • Use TypeScript generics for type parameters — not any or unknown escape hatches.
  • Prefer composition over inheritance. Public surfaces are interfaces callers can implement.
  • Avoid closed unions where module augmentation or registry patterns are possible.
  • Preserve upstream subpath exports — every public module must be tree-shakeable.
  • Merge upstream and downstream improvements. The migrated package gains every enhancement from both sides.

Anti-patterns that block approval

Anti-pattern Why it fails
"Fresh build" with a different API Consumers expecting upstream contracts break silently.
export * in any non-root barrel Banned — the barrel IS the contract; convert to explicit named exports inside the migration commit, not a follow-up.
Direct runner imports in non-exempt test files Tests must route through the runner-neutral wrapper; rewrite during the migration commit.
Dropping schema-driven features for "simplicity" Callers abandon the package and hand-roll the missing logic.
Replacing upstream errors with plain Error Loses programmatic handling, telemetry, and i18n keys.
Ignoring upstream enterprise audit The audit flagged real gaps; ignoring them recreates them.

Reference docs you must read

Before migrating any package:

  1. The canonical upstream capability spec.
  2. Any upstream change proposals (active and archived) that modified the package.
  3. Any upstream enterprise audit with coverage / error / quality gaps.
  4. Any design docs referenced by those proposals.

If any are missing, note it in the parity checklist and explain the risk.


0b. Ecosystem awareness — read your status dashboard

Before writing a new package or migrating a library, read the root status file (e.g. STATUS.md) to learn which packages already exist, which are completed, and which are pending.

Rules:

  • If your package depends on something in Pending, that dependency must be migrated first (or a compatible substitute must already exist). Do not hand-roll a replacement without explicit justification in the parity checklist.
  • If the package itself is in Pending, it must have an OpenSpec change written first (proposal.md + tasks.md) before any code.
  • If the package does not exist anywhere, it is an original. It still must have an OpenSpec change written first.
  • Never start coding without either (a) completed migration of all pending dependencies, or (b) an approved proposal that explicitly scopes out missing dependencies with architectural justification.

Checking the status file prevents duplicate work, broken downstream consumers, and incompatible re-implementations.


0c. Authoring the change — folder structure and spec-delta format

Read this before you create a single file in openspec/changes/. Every rule here is a gate.

The change folder has exactly this shape

openspec/changes/<change-id>/
  proposal.md                       # what & why, impact, affected dependents
  tasks.md                          # ordered implementation checklist
  design.md                         # OPTIONAL — non-normative design: rationale, algorithms,
                                    # code sketches, file layout, diagrams, trade-offs
  specs/<capability>/spec.md        # the SPEC DELTA — normative requirements only
  • <capability> is the package short name without any scope prefix. One capability folder per package. Mirrors the eventual canonical path openspec/specs/<capability>/spec.md.
  • design.md is optional but strongly preferred for any non-trivial package. It is the home for everything that is not a testable requirement: architecture, algorithm notes, file layout, integration narrative, benchmark plans, decisions & trade-offs. If you're writing prose that isn't a ### Requirement:, it belongs in design.md, not the spec delta.

openspec/specs/ is OFF LIMITS until archival

openspec/specs/<capability>/spec.md is the canonical, shipped spec. It is written to only by the §14 archival fold step, after all gates pass. While a change is PROPOSED or in progress:

  • NEVER create or edit anything under openspec/specs/. Not a stub, not a placeholder, not a "draft for reference". If openspec/specs/<your-capability>/ exists before your change archives, delete it and move the content into your change's specs/<capability>/spec.md delta.
  • The file inside your change folder is a delta, not the canonical spec. It is folded into openspec/specs/ on archive and the change folder is moved to openspec/archive/.

The spec delta is a normative requirements document — not an architecture dump

The delta MUST use the OpenSpec requirement/scenario format. Skeleton:

<!-- markdownlint-disable MD024 -->

# Capability Spec Delta: <capability>

**Package**: `<pkg>`
**Status**: PROPOSED

> Every requirement below carries a `**Verified by:**` line at authoring time. No exceptions.

## ADDED Requirements

### Requirement: <Imperative name>

The package SHALL … / MUST … — normative, testable prose. One requirement = one capability.

#### Scenario: <observable behaviour>

- **WHEN** <precondition / action>
- **THEN** <observable, checkable outcome>

**Verified by:** `src/<feature>/<feature>.test.ts::"<planned test name>"` AND `<another planned test>`

---

### Requirement: <next one>
…

Rules for the delta:

  • Top-level headings are exactly ## ADDED Requirements, ## MODIFIED Requirements, and/or ## REMOVED Requirements — nothing else. A fresh package uses only ## ADDED Requirements.
  • Every ### Requirement: has normative prose (SHALL / MUST / SHALL NOT), one or more #### Scenario: blocks as - **WHEN** / - **THEN** bullets, and exactly one **Verified by:** line. Separate requirements with ---.
  • **Verified by:** is authored at draft time. The change is not implemented yet, so it names planned test files (paths from tasks.md / design.md file layout) in path::"test name" form. That is expected and correct — it is the contract the implementation must satisfy.
  • Cover the whole contract: public API surface, every error code (each must be reachable from a real call site), boundary validation, telemetry instrumentation, AbortSignal propagation, subpath exports, tier purity (universal → no browser/Node APIs; browser → no Node; server → no browser). Match the package's depth — a large package is 15–25 requirements, a small leaf is 8–12.

proposal.md and tasks.md obligations

  • proposal.md MUST link its design and delta in the header block (- **Design**: ./design.md, - **Spec delta**: ./specs/<capability>/spec.md) — never a Reference spec: pointer into openspec/specs/. It MUST carry the ## Impact → ### Affected dependents section per §14a.
  • tasks.md MUST end with a "Fold specs/<capability>/spec.md into openspec/specs/<capability>/spec.md" step immediately before the archive step (see §14).

Multi-package / cross-cutting feature sets

When one feature spans many packages (a family, a wave of transports, etc.):

  • One change per package. Each package gets its own openspec/changes/<id>/ with its own specs/<capability>/spec.md delta. Do not write a single umbrella spec covering the whole family, and do not put a shared specs/<family>/ folder anywhere.
  • Shared architecture lives once, in the foundation package's design.md. Sibling design.md files reference it by relative path rather than duplicating.
  • Each change still stands alone: its proposal.md, design.md, tasks.md, and delta are self-sufficient.

Pre-flight checklist — before you write any change file

  1. The package is in the project status dashboard, or is an original — confirmed per §0b.
  2. You have read the upstream references in §0a (for migrations) or the design inputs (for originals).
  3. You will create proposal.md + tasks.md + specs/<capability>/spec.md (+ design.md), and nothing under openspec/specs/.
  4. The delta will be in ## ADDED/MODIFIED/REMOVED Requirements form with ### Requirement: / #### Scenario: / **Verified by:** — not an API dump.
  5. proposal.md will carry ### Affected dependents (§14a); tasks.md will carry the fold step (§14).
  6. You have created a numbered checklist of every ### Requirement and every #### Scenario in the spec delta. This lives in your working notes. You may not begin implementation until every requirement is listed as a discrete, checkable item. As you implement, check each off. An unchecked item at the end means the package is not done. Never silently skip a requirement because "tests already pass without it." That is the single most common cause of spec drift.

1. Dependency conventions

  • All external deps use catalog refs — never bare version strings like "^1.2.3".
  • All internal deps use workspace:^.
  • If a catalog key does not exist, add it to the root manifest first.
  • Coverage provider must be installed as a devDependency via catalog ref. Without @vitest/coverage-istanbul (or equivalent), Istanbul-style coverage silently falls back to v8.
  • Testing-infra packages have zero internal workspace deps.

2. No banned patterns in src/

Before closing any task, scan for zero matches:

grep -rn "@deprecated\|TODO\|FIXME\|ts-ignore\|ts-expect-error\|\.skip\|\.only\|\bany\b\|console\.log" \
  packages/<pkg>/src/ --include="*.ts"
  • @deprecated → prose: "Backwards-compatibility alias for X."
  • any → unknown + narrowing, or a proper generic.
  • console.log → real logger / injectable LoggerLike.

2a. Lint suppressions

Stale // biome-ignore / // eslint-disable* comments break the lint gate hard.

Rules:

  1. Never write a suppression comment for transient lint noise. If the linter flags something, it's a real signal — fix the code.
  2. If a deliberate violation is required, use a coding pattern the linter cannot statically reject (see §8.4 of GENERIC-CLAUDE.md).
  3. If a suppression genuinely is the only path, pin both the current rule category and a one-sentence reason. Verify the category resolves before committing.
  4. Scan before closing any task: grep -rn "biome-ignore\|eslint-disable" packages/<pkg>/src/ --include="*.ts". Every hit MUST be justified in code review.

2b. NEVER ship stubs, placeholders, or "WIP" code

See GENERIC-CLAUDE.md §8.1 for the full list. Banned: stub functions, placeholder types, TODO/FIXME, "Phase 2 unwritten" splits, marketing language masking incomplete work, tautological tests, stub docs, empty test files, silently dropped spec requirements.

If you discover an existing stub during audit (40-line README that fails docs:check, a function with return undefined as never, a barrel re-exporting nothing), the right answer is to fix it inside the same change as your other work. Leaving it is a vote for the placeholder.

2c. NEVER delete spec elements to satisfy a gate — wire them in

When a gate fails because a registered element (error code, schema, event topic, requirement, capability constant) is declared but unused, the failure is telling you the implementation hasn't caught up to the proposal. The fix is to wire the element into the call site the proposal designed it for, NOT to delete the element from the registry.

Deletion is the path of least resistance and silently strips features the proposal committed to. The proposal is the contract; the registry is the proposal expressed in code. Removing an entry to make a tool happy is the same defect as marking a failing test .skip to make CI green.

Concretely banned:

  • Removing an error code from the codes registry because conformance check flags it as unused. Find the call site that should be raising / reporting it and wire it in.
  • Deleting a schema field because no caller populates it yet.
  • Removing an event topic because no emitter exists. Add the emitter; the topic is an API surface the proposal owns.
  • Quietly removing a ## ADDED Requirements line because the implementation doesn't cover it. Either implement it or open a follow-up change that explicitly defers it with reasoning — never silently retract.
  • Lowering coverage / docs-check thresholds to make a gate pass.

When in doubt, ask: "Was this element part of the proposal I'm implementing?" If yes, wire it in. If genuinely no (it crept in by mistake), removing it requires a one-line note in proposal.md under ### Deferred or ### Removed so the deletion is auditable.


3. File-size budget, documentation, README

See GENERIC-CLAUDE.md §§9, 11. Summary:

  • Source files 300 lines hard / 250 soft; test files 500 max; functions 50 soft.
  • Every .ts impl file needs a sibling .md with Purpose / Features / Basic / Advanced / API / Implementation Notes (≥200 words excl. code).
  • Doc-drift gate: changing a .ts requires touching the sibling .md in the same change set when the public surface or documented behavior changes.
  • Root README.md per package: name, description, install, quick-start, Modules link list.

4. Subpath exports per feature module

Every feature directory must have its own entry in package.json exports. A single "." barrel is insufficient. The subpath count should match the feature module count.

Verify entry points import nothing with side effects (telemetry registration, global state mutations). A sideEffects: false declaration with a leaking side effect is a silent bug.


5. Barrel exports — explicit named only

See GENERIC-CLAUDE.md §6.4. Feature barrels MUST use explicit named exports; export * is allowed only in generated forwarder files and top-level namespace re-exports.


6. Coverage thresholds

100 / 100 / 100 / 100 for every package, no exceptions, no "foundation only" carve-out:

thresholds: { statements: 100, branches: 100, functions: 100, lines: 100 }

The pre-publish gate MUST hard-fail below 100%. Mutation, contract, and property/fuzz testing layer on top — they do not substitute, and 100% line coverage does not substitute for them.

Never delete code to make coverage pass

See GENERIC-CLAUDE.md §8.3. When a path is uncovered, write a test that exercises real behavior — do NOT delete defensive guards, rollback helpers, or catch arms.


7. Test runner imports — mandatory

All test files MUST import runner primitives (describe, it, expect, vi, beforeAll/beforeEach/afterAll/afterEach, etc.) from a single project-wide neutral wrapper that re-exports the active runner via conditional exports:

// CORRECT — runner-neutral
import { describe, expect, it, vi } from "<project>/testing/runner";

// BANNED — pins the test file to one runner
import { describe, expect, it } from "bun:test";
import { describe, expect, it } from "vitest";

The wrapper is a conditional-export barrel: bun condition → re-exports bun:test; default / vitest condition → re-exports vitest. Tests then execute identically under either runner.

Exempt allowlist: only the wrapper-authoring packages (the testing-infra packages themselves). Adding a package to the allowlist requires its own OpenSpec change with justification.

Migration prerequisite: when migrating any library from upstream, every from "vitest" / from "bun:test" import MUST be rewritten to the wrapper inside the migration commit, not as a follow-up. Same rule applies when porting test files between packages or when an LLM agent generates a fresh test file.

Verification grep (zero output for any non-exempt package):

grep -rnE "from ['\"](vitest|bun:test)['\"]" \
  packages/<pkg>/src/ --include="*.ts" --include="*.tsx"

A repo-wide CI lint gate must run this across every non-exempt package on every commit. Code review must independently flag direct runner imports — relying solely on CI is how 19 packages drifted before the gate existed.

Test structure

Arrange-Act-Assert. Name tests after behavior, not implementation. See GENERIC-CLAUDE.md §10.2.


8. Task-runner filtering reference

<task-runner> test --filter=<pkg>          # one package
<task-runner> test --filter=<pkg>...       # pkg + its deps
<task-runner> test --filter=...<pkg>       # pkg + its dependents
<task-runner> test --filter=...[main]      # changed since main
<task-runner> test --filter=...[HEAD^1]    # changed in last commit
<task-runner> test --filter={a,b,c}        # multiple (OR)
<task-runner> test --filter=!<pkg>         # exclude

9. Benchmarks

Every package should include benchmarks in src/__benchmarks__/. Use Markdown profiling output (CPU + heap) for LLM-assisted analysis. Publish baseline numbers in README.


10. Non-redundant path naming

A file or folder name must not contain the singular or plural form of any ancestor folder. See GENERIC-CLAUDE.md §6.2 for the rule and table.

Check:

find packages/<pkg>/src -type d | awk -F/ '
  NF>=2 {
    parent=$(NF-1); child=$NF
    singular=parent; sub(/ies$/, "y", singular); sub(/s$/, "", singular)
    if (index(child, singular) > 0) print $0
  }
'

11. Type design — no inline shapes

See GENERIC-CLAUDE.md §12. Named interfaces/types only — no nested object literals in interfaces; no inline shapes in function signatures. Cross-package types live in the shared zero-dep types package.


12. Clean workspace

Before declaring any task done:

  • Remove scratch files, debug outputs, temporary artifacts.
  • Verify git status shows only intentional changes.

12a. Spec-completeness verification — before any gate

Never trust a compaction summary, archive status, or status-dashboard badge as proof that work is complete. The only source of truth is the change's own tasks.md and design.md.

Before running ANY command in §13, manually:

  1. Open the change's tasks.md. For migrations, also open the upstream tasks.md.
  2. Walk every task item. For each item that claims a file/module/type/feature exists, verify it is present. A task that says "Implement geo/distance — greatCircleDistance and vincentyDistance" means BOTH functions must exist, have tests, have docs, and be exported from the barrel.
  3. For migrations, compare to upstream. Diff the upstream src/ against your equivalent. If upstream has providers/admiralty/admiralty-provider.ts with real fetch logic and yours has a 3-line stub, the migration is incomplete.
  4. Empty directories are a red flag, not cleanup. If a directory exists in src/ and is empty, it was scaffolded for a feature that was never implemented. Implement and populate it — do NOT delete the directory to make the tree look tidy.
  5. Cross-check against the parity checklist. Every 🚫 row MUST have a written justification; every ⬆️ row must be real. If the table disagrees with the file tree, the table is wrong — fix the implementation, not the table.

If any task item is unchecked or unverified, the change is not done. Do not proceed to §13, do not archive, do not update the status dashboard.


13. Pre-ship verification sequence

Run in order — all must be green. Step 0 is a hard gate; do not skip it.

# 0. SPEC RECONCILIATION — NON-NEGOTIABLE. Run BEFORE any other gate.
#    Walk every `### Requirement` and `#### Scenario` in the spec delta.
#    For each requirement, grep the implementation to confirm the named
#    class, function, constant, span, counter, histogram, event, or error
#    code actually exists in src/. For each `**Verified by:**`, confirm
#    the test file exists and the test name matches *verbatim*.
#    If any requirement is unimplemented or any Verified-by is broken,
#    STOP. Do not run step 1.

# 1. Lint + typecheck + tests + format
<task-runner> lint typecheck test --filter=<pkg>
<formatter> check  # e.g. biome check --write --unsafe src/

# 2. Coverage at 100%
<task-runner> test:coverage --filter=<pkg>

# 3. Build
<task-runner> build --filter=<pkg>

# 4. Banned-pattern scan (zero output expected)
grep -rn "@deprecated\|TODO\|FIXME\|ts-ignore\|ts-expect-error\|\.skip\|\.only" \
  packages/<pkg>/src/ --include="*.ts"

# 4a. Barrel-shape scan (zero output for any feature barrel)
grep -rn "^\s*export\s\+\(type\s\+\)\?\*\s\+from" \
  packages/<pkg>/src/ --include="index.ts" \
  | grep -v "/src/index\.ts:" \
  | grep -v "\.select/" \
  | grep -v "export \* as"

# 4b. Test-runner import scan (zero output for non-exempt packages)
grep -rnE "from ['\"](vitest|bun:test)['\"]" \
  packages/<pkg>/src/ --include="*.ts" --include="*.tsx"

# 5. Missing .md scan
for f in $(find packages/<pkg>/src -name "*.ts" \
  ! -name "*.test.ts" ! -name "*.spec.ts" ! -name "*.types.ts" \
  ! -name "index.ts"); do
  md="${f%.ts}.md"; [ -f "$md" ] || echo "MISSING: $md"
done

# 6. README check
test -f packages/<pkg>/README.md && echo "README OK" || echo "MISSING README"

# 7. Bare-version check (zero output expected)
node -e "
  const p = JSON.parse(require('fs').readFileSync('packages/<pkg>/package.json','utf8'));
  const all = {...p.dependencies,...p.devDependencies,...p.peerDependencies};
  for (const [k,v] of Object.entries(all ?? {})) {
    if (typeof v === 'string' && /^[\^~]?\d/.test(v)) console.log('BARE VERSION:', k, v);
  }
"

# 8. Subpath export count
node -e "
  const p = JSON.parse(require('fs').readFileSync('packages/<pkg>/package.json','utf8'));
  const sub = Object.keys(p.exports ?? {}).filter(k => k.startsWith('./') && k !== '.');
  console.log('Subpath exports:', sub.length, sub);
"

# 9. Security scan — hardcoded secrets (zero output expected)
grep -rn \
  "password\s*=\s*['\"][^'\"]\|secret\s*=\s*['\"][^'\"]\|apiKey\s*=\s*['\"][^'\"]\|token\s*=\s*['\"][^'\"]" \
  packages/<pkg>/src/ --include="*.ts"

# 10. Dependent sweep — mandatory for foundation packages
<task-runner> test --filter=...<pkg>

For foundation packages (widely-consumed: error / types / events / logger / validation in this project's case), step 10 is mandatory — catches API-shape regressions BEFORE shipping. Green local tests do not prove you didn't break consumers.

For non-foundation packages, step 10 is optional — but if you altered the public surface, run it anyway. The cost is one command; the cost of skipping is finding out from a consumer's CI next week.


13a. Behavior-change propagation

See GENERIC-CLAUDE.md §19. When a change introduces consumer-visible behavior changes (new error code, renamed metric, method that now throws, default flipped, return shape change), you MUST:

  1. Self-doc: update sibling .md + README.md Features section.
  2. Dependent surface scan: grep -rln "<old-name>" packages/ | grep -v "/dist/\|/<own-pkg>/". Every hit needs updating in the same change set.
  3. Migration note: add ## Migration to proposal.md listing (a) old behavior, (b) new behavior, (c) consumer search command, (d) recommended replacement.

Skipping any of these means a consumer somewhere is acting on an obsolete contract.


13b. Root-doc refresh + markdown-lint

Once §13 is green, but before archival (§14), refresh the repo-root manifest docs and run the markdown-lint pass.

Mandatory updates:

  • Auto-generated manifests (MANIFEST.md, MISSING.yml, dep-graph indexes): run the generators.
  • Hand-edited status dashboard (e.g. STATUS.md): move the package row from Pending → Completed, keeping icon and link conventions.
  • Hand-edited summary doc (e.g. SUMMARY.md): update or add the per-package paragraph to reflect the shipped surface.
  • Migration narrative (e.g. MIGRATION.md): update only if the change affects it.

Then auto-fix markdown:

bunx markdownlint-cli2 --fix

Re-stage anything the lint pass modified. Verify git status shows only intentional changes before archiving. If unfixable violations remain, resolve them by hand — never ship with lint failures, never silence with inline disables.

Verification before archival:

git status              # only intentional changes
git diff MANIFEST.md MISSING.yml STATUS.md SUMMARY.md MIGRATION.md
bunx markdownlint-cli2  # second pass, no --fix — must exit 0

14. OpenSpec archival

After all gates pass:

  1. Fold the spec delta into openspec/specs/<capability>/spec.md.
  2. Move openspec/changes/<change-id>/ → openspec/archive/<yyyy-mm-dd>-<change-id>/.

14a. Proposal must list affected dependents

Every proposal.md MUST contain an ## Impact section that explicitly enumerates downstream packages and what was done in each:

## Impact

### Affected dependents

Run `<task-runner> ls --filter=...<pkg>` to enumerate. For each dependent, check one box:

- `<dep-1>` — [ ] tests run / [ ] docs reviewed / [x] no action needed (reason: …)
- `<dep-2>` — [x] tests run / [x] docs reviewed / [ ] no action needed
- `<dep-3>` — …

If `tests run` is unchecked for any dependent, the change MUST NOT be folded into the canonical spec — finish that work first or split the proposal.

The "no action needed" path requires a one-line reason (e.g. "consumer doesn't touch the changed surface"). It is NOT a free pass; it documents the conscious decision for the next agent reading the archive.

For foundation packages, the Affected-dependents list is non-optional. For non-foundation packages with zero dependents, write: "No dependents (verified by <task-runner> ls --filter=...<pkg>)."

Combined with §13 step 10 (dependent sweep) and §13a (behavior-change propagation), this closes the loop: the proposal names the blast radius, the verification proves the blast radius compiles and tests, and the migration note tells future readers what changed and how to adapt.