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

534 lines
28 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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:
```markdown
| 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
```text
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:
```markdown
<!-- 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:
```bash
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:
```ts
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:
```ts
// 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):
```bash
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
```bash
<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:
```bash
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.**
```bash
# 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:
```bash
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:
```bash
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:
```markdown
## 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.