Skip to content

Commit 3d66bfd

Browse files
itcmsgrclaude
andcommitted
docs(v1.100 PR-25): contract — append §§16-29 PR-25 execution contract
Appends the PR-25 execution contract to internal/installer/restore/ contract.md as a new "PART II — PR-25 execution contract" section (§§16-29). This is the doc-only first PR of the PR-25 two-PR split, mirroring the PR-24 PR #493 → PR #494 pattern. The implementation PR opens in a separate branch after this one merges. Origin: The contract is a faithful normalization of the locked Q1-Q5 design decisions (recorded 2026-04-20 during PR-24 freeze Day 0 via the §12 protocol: Stage 1 scope classification + Stage 2 five-field answer + LOCK/REVISE/REJECT review). The v0 staging sheet (memory/project_pr25_contract_sheet_v0.md) was reviewed and locked 2026-04-27 prior to opening this PR. Locked rule applied: "Normalize, do not expand." Every clause in §§16-29 traces back to a Q1-Q5 lock or to V1100 contract §8. No design decisions were made in this PR. Section map: - §16 Purpose - §17 Scope (Option A) + 2 named invariants - §18 TargetAuthority concretization (Q2) - §19 StateRestoreDecided downstream meaning (Q3) - §20 Panel-auto target consistency (Q4) - §21 Post-restore verification split (Q5) - §22 State terminals + exit codes (candidates) - §23 Execution shape (V1100 §8 ordered) - §24 Inputs PR-25 may consume - §25 Forbidden behaviors (consolidated) - §26 Cross-lock consistency - §27 What this contract does NOT contain (intentional) - §28 Merge-blocking real-host matrix (code phase) - §29 Reviewer checklist (code phase) Verified live code anchors (2026-04-27): - knownFirewallType set {ufw, firewalld, iptables, csf} at internal/installer/uninstall/prior.go:278-284 - writeHistory gate excluding cfg.mode == "restore" at cmd/nftban-installer/main.go:132 - Exit-code constants ExitCommitted=0/ExitFatal=4/ExitRefused=5/ ExitIntentRequired=6 at internal/installer/state/machine.go:149-155 §1-§15 (PR-24 decision contract) are untouched. Out of scope (locked): - No code in this PR. PR-25 implementation is the next PR (feat/v1.100-pr25-restore-execution). - No expansion of Q1-Q5 lock content. - PR-26 contract stays out of scope. Lifecycle completion lane (PR-25..PR-30) remains explicitly OPEN but is now mid-re-entry: contract is the first deliberate step. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 5ea5ace commit 3d66bfd

2 files changed

Lines changed: 337 additions & 0 deletions

File tree

CHANGELOG.md

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,44 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1111
1212
---
1313

14+
## [Unreleased] - v1.100 PR-25 contract sheet (doc only)
15+
16+
Appends the PR-25 execution contract to `internal/installer/restore/contract.md` as a new "PART II — PR-25 execution contract" section (§§16–29). This is the doc-only first PR of the PR-25 two-PR split (mirrors the PR-24 PR #493 → PR #494 pattern). The implementation PR opens in a separate branch after this one merges.
17+
18+
### Origin
19+
20+
The contract is a faithful normalization of the **locked Q1–Q5 design decisions** recorded 2026-04-20 during PR-24 freeze Day 0 via the §12 protocol (Stage 1 scope classification + Stage 2 five-field answer + LOCK/REVISE/REJECT review). The v0 staging sheet (`memory/project_pr25_contract_sheet_v0.md`) was reviewed and locked 2026-04-27 prior to opening this PR.
21+
22+
### Locked rule applied
23+
24+
> *"Normalize, do not expand."*
25+
26+
Every clause in §§16–29 traces back to a Q1–Q5 lock or to V1100 contract §8. No design decisions were made in this PR. Items intentionally absent (final state-terminal names, final exit-code integers, test fixture matrix, real-host evidence plan, CI gate expansion plan, concrete `IsRestoreExecuted` signature, the PanelType→firewall mapping body) are documented in §27 as code-phase work.
27+
28+
### What changed
29+
30+
- `internal/installer/restore/contract.md` — appended §§16–29 (PR-25 execution contract) after §15 and before "Amendment history"; §1–§15 (PR-24 decision contract) untouched. New "PART II" header marks the boundary. Amendment history gains a 2026-04-27 v2 entry documenting the append + verified code anchors.
31+
32+
### Verified code anchors (2026-04-27)
33+
34+
| Lock reference | Verified at |
35+
|---|---|
36+
| `knownFirewallType` set `{ufw, firewalld, iptables, csf}` | `internal/installer/uninstall/prior.go:278-284` |
37+
| writeHistory gate excluding `cfg.mode == "restore"` | `cmd/nftban-installer/main.go:132` |
38+
| Exit-code constants `ExitCommitted=0`, `ExitFatal=4`, `ExitRefused=5`, `ExitIntentRequired=6` | `internal/installer/state/machine.go:149-155` |
39+
40+
All three live in surfaces that were fenced under the PR-24 freeze and remained untouched throughout the GOTH removal + SF-1 + repo hygiene stabilization train.
41+
42+
### Out of scope (locked)
43+
44+
- **No code in this PR.** PR-25 implementation (Go types, execution engine, inline safety interlock, state terminals, tests, CI gate update) is the *next* PR (`feat/v1.100-pr25-restore-execution`) and opens only after this contract PR merges.
45+
- **No expansion** of Q1–Q5 lock content.
46+
- PR-26 contract content stays out of scope (defined by PR-26's own seed work).
47+
48+
Lifecycle completion lane (PR-25..PR-30) remains explicitly **OPEN** but is now mid-re-entry: contract is the first deliberate step.
49+
50+
---
51+
1452
## [Unreleased] - v1.100.3e Repo hygiene Phase A slice 1e (H-07 + H-08)
1553

1654
Closes audit findings **H-07** (STATUS.md version drift) and **H-08** (README.md badge version drift). Both displayed visible-version strings that no longer matched `/VERSION` (1.98.2). STATUS.md claimed v1.89.0 (-9 minor versions); README badge pinned 1.95.0 (-3 patches).

internal/installer/restore/contract.md

Lines changed: 299 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -332,6 +332,303 @@ Four new gates in the `G4-RESTORE-*` namespace:
332332

333333
---
334334

335+
# PART II — PR-25 execution contract
336+
337+
> **Boundary rule (locked):** PR-24 decides. PR-25 executes. PR-25 may never reinterpret PR-24.
338+
>
339+
> Sections §16–§29 below are normative for PR-25 only. They do not modify §1–§15.
340+
341+
## 16. Purpose (PR-25)
342+
343+
PR-25 is the **execution** counterpart of PR-24's decision engine.
344+
345+
| Phase | Owner |
346+
|---|---|
347+
| Decision (PROCEED / REFUSE / REQUIRE_EXPLICIT_INTENT) | PR-24 (§§1–15 above; merged, soak-validated) |
348+
| Execution (mutate kernel + service run-state to fulfil the authorized decision) | **PR-25 (§§16–29 below)** |
349+
| Post-restore verification gate | PR-26 (separate scope) |
350+
351+
## 17. Scope — Option A: restore execution only (locked, Q1)
352+
353+
PR-25 is **restore execution only**. Bundled purge / remove / artifact-cleanup semantics are EXPLICITLY excluded.
354+
355+
### 17.1 Permitted (PR-25 may do)
356+
357+
- Kernel `nft` mutations within the authorized target's surface
358+
- Service **run-state** changes: `start` / `stop` of `nftband.service` and the target-native firewall service (`ufw` / `firewalld` / `iptables` / `csf` per §20)
359+
- Emergency-SSH safety net: insertion before mutation, removal after inline verification (§21)
360+
- New execution-outcome state terminals + exit codes (§22)
361+
362+
### 17.2 Forbidden (PR-25 may NOT do)
363+
364+
| Forbidden surface | Why excluded |
365+
|---|---|
366+
| `enable` / `disable` policy mutation | INV-PR25-STATIC-SERVICE-LIFECYCLE |
367+
| `mask` / `unmask` | Same |
368+
| Unit-file mutation | Same |
369+
| Cross-target service orchestration | Authority-isolation invariant |
370+
| Filesystem artifact cleanup | Q1 forbidden — deferred to a future purge contract |
371+
| `.conf.local` policy mutation | Operator-content surface; not lifecycle |
372+
| History-schema changes | Stability of decision-record layer |
373+
| `--purge` flag semantics | Q1 forbidden |
374+
| Post-restore verification gate | Q5-B → PR-26 |
375+
| Panel-auto target-identity *resolution* | Q4 — execution-time mapping only |
376+
| Shell wrapper code | Implementation surface choice |
377+
378+
### 17.3 Scope-bounding invariants (named, locked)
379+
380+
- **INV-PR25-AUTHORITY-IMMUTABILITY**: `TargetAuthority` resolved by PR-24 PROCEED is unchanged across the entire PR-25 execution window. No mid-flight re-resolution.
381+
- **INV-PR25-STATIC-SERVICE-LIFECYCLE**: PR-25 makes no change to systemd unit policy, enable/disable state beyond run-state, or unit files.
382+
383+
## 18. TargetAuthority concretization (locked, Q2)
384+
385+
`TargetAuthority` is a struct with **unexported fields** and read-only accessors. Construction is constrained to one of three paths.
386+
387+
### 18.1 Type definition (canonical)
388+
389+
```go
390+
type TargetAuthorityKind string
391+
392+
const (
393+
TargetAuthorityKindNone TargetAuthorityKind = "" // zero value
394+
TargetAuthorityKindRecordedPrior TargetAuthorityKind = "recorded_prior"
395+
TargetAuthorityKindPanelNative TargetAuthorityKind = "panel_native"
396+
)
397+
398+
type TargetAuthority struct {
399+
kind TargetAuthorityKind // unexported
400+
firewallType string // unexported
401+
panel detect.PanelType // unexported
402+
}
403+
404+
// Read-only accessors
405+
func (t TargetAuthority) Kind() TargetAuthorityKind
406+
func (t TargetAuthority) FirewallType() string
407+
func (t TargetAuthority) Panel() detect.PanelType
408+
409+
// Per-Kind constructors
410+
func TargetNone() TargetAuthority
411+
func TargetRecordedPrior(firewallType string) (TargetAuthority, error)
412+
func TargetPanelNative(panel detect.PanelType) (TargetAuthority, error)
413+
```
414+
415+
### 18.2 Construction paths
416+
417+
| Kind | Reachable via | Validation |
418+
|---|---|---|
419+
| `None` | `TargetNone()` OR zero value `TargetAuthority{}` (equivalent and intentional) | n/a |
420+
| `RecordedPrior` | ONLY `TargetRecordedPrior(firewallType)` | `firewallType ∈ knownFirewallType` set defined at `internal/installer/uninstall/prior.go:278-284`: `{ufw, firewalld, iptables, csf}` |
421+
| `PanelNative` | ONLY `TargetPanelNative(panel)` | `panel ≠ detect.PanelNone` |
422+
423+
### 18.3 Payload invariants (per Kind)
424+
425+
| Kind | Required fields shape |
426+
|---|---|
427+
| `None` | `firewallType=""`, `panel=PanelNone` |
428+
| `RecordedPrior` | `firewallType ∈ known set`, `panel=PanelNone` |
429+
| `PanelNative` | `firewallType=""`, `panel ≠ PanelNone` |
430+
431+
### 18.4 Closed-enum discipline
432+
433+
- `Kind` enum is closed at the type level. Adding a variant requires §12-style review.
434+
- Default branch in any `Kind` switch MUST `panic` at runtime.
435+
- No exported mutators. Post-construction immutability is by type design — not enforced by CI/linter (acknowledged limit).
436+
437+
## 19. StateRestoreDecided downstream meaning (locked, Q3)
438+
439+
### 19.1 Semantic rule
440+
441+
> *"Exit code 0 from `--mode=restore` with `StateRestoreDecided` is a policy-handoff outcome only. It is NOT evidence that any restoration mutation has been executed."*
442+
443+
### 19.2 Four enforcement layers
444+
445+
| Layer | What | Where |
446+
|---|---|---|
447+
| 1 — Type-level distinctness (narrow) | `StateRestoreDecided` is a distinct constant; PR-25 execution-terminals are **separate** constants. Prevents equality-based confusion only. | `internal/installer/state/machine.go` |
448+
| 2 — API-level disambiguation (PR-25, optional) | `IsRestoreExecuted(s State) bool` helper. Returns true ONLY for execution-terminals. Never true for `StateRestoreDecided`. | new helper alongside `IsApplyTerminal` |
449+
| 3 — Contract-level documentation | Consumer-facing rule added to `state/machine.go` comment + this document. Covers the exit-code-misread class. | (this section) |
450+
| 4 — Defensive gate (already landed) | `cfg.mode == "restore"` excluded from history-write at `cmd/nftban-installer/main.go:132`. | (existing) |
451+
452+
### 19.3 Consumer rule
453+
454+
Consumers MUST NOT use `sf.State == StateRestoreDecided` to infer that restoration execution has occurred.
455+
456+
### 19.4 Exit code discipline
457+
458+
PR-25 execution terminals MUST carry distinct exit codes, **separate from** the existing constants at `internal/installer/state/machine.go:149-155`:
459+
460+
- `ExitCommitted = 0`
461+
- `ExitFatal = 4`
462+
- `ExitRefused = 5`
463+
- `ExitIntentRequired = 6`
464+
465+
New PR-25 exit codes are allocated from the next available integers.
466+
467+
## 20. Panel-auto target consistency (locked, Q4)
468+
469+
When `TargetAuthority.Kind == PanelNative`, PR-25 execution resolves the firewall via a **static explicit mapping**:
470+
471+
```
472+
target_firewall = mapping[TargetAuthority.Panel()]
473+
```
474+
475+
### 20.1 Mapping properties
476+
477+
- **Static, compile-time.** Shipped with PR-25 code as a const map or exhaustive switch.
478+
- **Key type:** `detect.PanelType`.
479+
- **Output validation rule:** mapped value MUST be a member of `knownFirewallType` (the same set used in §18.2).
480+
- **Not required to be exhaustive** over all `detect.PanelType` values.
481+
482+
### 20.2 Ambiguity resolution
483+
484+
| Case | PR-25 behavior |
485+
|---|---|
486+
| Panel ∈ mapping | Execute the mapped firewall |
487+
| Panel ∉ mapping | **REFUSE before any mutation.** Transition to PR-25 execution-failure terminal (see §22). |
488+
489+
### 20.3 Forbidden fallbacks
490+
491+
- **No fallback** to a default firewall on unmapped panel.
492+
- **No fallback** to recorded-prior `FirewallType` (structurally empty by §18.3 invariant when Kind=PanelNative; AND contractually forbidden to consult).
493+
- **No heuristic**, **no runtime discovery**, **no config-driven mapping**.
494+
495+
### 20.4 Boundary with PR-24
496+
497+
§20 is **execution-time** resolution. PR-24 decision-time behavior is NOT modified.
498+
499+
## 21. Post-restore verification — split (locked, Q5)
500+
501+
### 21.1 PR-25 — INLINE VERIFICATION = safety interlock, NOT a gate
502+
503+
- **Purpose:** Prove enough about the mutation outcome to *truthfully set the execution-terminal state* AND *safely remove the safety net*.
504+
- **Scope rule:** Minimum-sufficient checks for terminal-state + safety-net removal decisions.
505+
- **Coverage:** seed skeleton §11's first three assertions only:
506+
- Target firewall is active
507+
- nftban authority class is correct (post-mutation)
508+
- Safety-net removal is safe to proceed with
509+
- **Timing:** Runs WHILE the safety net is still present.
510+
- **Classification:** **safety interlock**, not a verification gate.
511+
512+
### 21.2 PR-26 — POST-RESTORE VERIFICATION GATE (scope-expanded, NOT in this contract)
513+
514+
PR-26 contract content is defined by PR-26's own contract seed work, NOT by §21.
515+
516+
What §21 records about PR-26:
517+
- PR-26 is **scope-expanded** from V1100 contract §8's "post-uninstall verification" / G3-UN-VERIFY to also cover post-restore outcomes.
518+
- PR-26 has its own verification-outcome terminals and exit codes.
519+
- PR-26 has its own operator-invokable CLI surface (PR-26 scope, NOT §21).
520+
- PR-26 is **scope-expanded, not replaced, renumbered, or split**.
521+
522+
### 21.3 Hard invariant
523+
524+
PR-25 MUST NOT remove the safety net until inline verification (§21.1) passes. (V1100 contract §8 step 5.)
525+
526+
### 21.4 PR-25 must NOT implement the gate
527+
528+
PR-25 implements only §21.1. The full gate is PR-26's contract scope.
529+
530+
## 22. State terminals + exit codes (PR-25 introduces)
531+
532+
Concrete names are open in the lock — confirmed candidate set, finalized in code phase:
533+
534+
| Candidate name | Meaning | Exit code |
535+
|---|---|---|
536+
| `StateRestoreExecuted` | Full mutation completed AND inline verification passed AND safety net removed | next available |
537+
| `StateRestoreFailedExecution` | Mutation failed mid-flight; safety net still present; system rolled to known-safe state | next |
538+
| `StateRestoreDegraded` | Mutation completed but inline verification flagged a soft-fail condition; safety net removed under explicit policy | next |
539+
| `StateRestoreFailedVerification` | Mutation completed but inline verification hard-failed; safety net retained; explicit operator action required | next |
540+
541+
Constraints:
542+
- All four MUST be NEW constants distinct from `StateRestoreDecided` (§19.2 layer 1).
543+
- Exit codes MUST be distinct from `ExitCommitted=0` / `ExitFatal=4` / `ExitRefused=5` / `ExitIntentRequired=6`.
544+
- `IsRestoreExecuted(s)` (§19.2 layer 2) returns true for `StateRestoreExecuted` and `StateRestoreDegraded`; false for the other two AND for `StateRestoreDecided`.
545+
546+
Final names + final integers chosen during the code phase.
547+
548+
## 23. Execution shape (V1100 §8, ordered)
549+
550+
PR-25 execution proceeds in this exact ordered sequence:
551+
552+
1. **Preflight target validation** — confirm authority resolution still satisfies invariants (§18.3 payload invariants); confirm `knownFirewallType` membership (RecordedPrior); confirm panel mapping resolves (PanelNative). Refuse here is non-mutating.
553+
2. **Safety net insertion** — emergency-SSH allow rule before any other mutation.
554+
3. **Minimal target-specific execution** — kernel nft mutations + service run-state changes for the authorized target only. No cross-target traffic.
555+
4. **Verification while safety net still present** — inline checks per §21.1.
556+
5. **Safety net removal only after verification passes** — hard gate, no exceptions.
557+
6. **Terminal state set truthfully** — pick the execution-success or execution-failure terminal that matches step-4 outcome; emit the corresponding exit code.
558+
559+
## 24. Inputs PR-25 may consume
560+
561+
- **PR-24 output** — decision MUST be `PROCEED`. `REFUSE` / `REQUIRE_EXPLICIT_INTENT` → PR-25 does not run.
562+
- **Resolved target identity**`TargetAuthority` of `Kind=RecordedPrior` (with `firewallType`) OR `Kind=PanelNative` (with `panel`). `Kind=None` → PR-25 does not run.
563+
- **Current classifier state** — must still be compatible at runtime (preflight at §23 step 1).
564+
- **Prior record** — only as already reduced/approved by the PR-24 path. PR-25 does not re-reduce.
565+
566+
PR-25 may **NOT** consume:
567+
- Any signal that re-derives `TargetAuthority` (locked by INV-PR25-AUTHORITY-IMMUTABILITY).
568+
- Operator config beyond what was already authorized by PR-24.
569+
570+
## 25. Forbidden behaviors (consolidated)
571+
572+
- No execution if PR-24 ≠ `PROCEED`.
573+
- No target inference beyond what PR-24 authorized.
574+
- No reinterpretation of `StateRestoreDecided` as success.
575+
- No silent fallback between targets.
576+
- No "try restore, then rebuild nftban if fails" pattern.
577+
- No hidden purge/remove unless explicitly in scope (it isn't).
578+
- No history-schema redesign unless separately approved.
579+
- No mutation outside the declared target surface.
580+
- No mid-flight `TargetAuthority` re-resolution.
581+
- No verification beyond §21.1 inline checks (gate is PR-26).
582+
- No safety-net removal before inline verification passes.
583+
584+
## 26. Cross-lock consistency
585+
586+
- **§17 narrow scope × §21 split** → PR-25 stays execution-only; gate-level verification stays in PR-26 → scope integrity preserved.
587+
- **§18 type safety × §20 panel resolution**`FirewallType` structurally empty for `PanelNative` (§18.3 invariant) AND forbidden to consult (§20.3 contract) → no accidental cross-variant authority leak.
588+
- **§17 authority-resolution immutability × §19 decision-vs-execution boundary**`TargetAuthority` set once by PR-24, read-only by PR-25 → no reinterpretation across the boundary.
589+
- **§19 rollback coupling × §21 inline safety interlock** → inline verification gates safety-net removal → prevents "remove before prove" rollback failure that would corrupt §19's decision-execution distinction.
590+
591+
## 27. What this contract does NOT contain (intentional, not omission)
592+
593+
- Final state-terminal names + final integer exit codes (§22 candidates only)
594+
- Test fixture matrix (parallels PR-24's exhaustive matrix; produced during code phase)
595+
- Real-host evidence plan (§28 below names hosts; the matrix itself is code-phase work)
596+
- CI gate expansion plan (minimal beyond what's already in `ci-restore-canonization.yml`; specifics emerge in code phase)
597+
- Concrete `IsRestoreExecuted` signature (helper is locked as §19.2 layer 2 optional API)
598+
- The PanelType → firewall mapping itself (locked as static; concrete map content is code-phase work)
599+
600+
These are intentionally absent. **Expansion would violate the locked rule "no expansion beyond Q1–Q5".**
601+
602+
## 28. Merge-blocking real-host matrix (PR-25 code-phase)
603+
604+
| Host | OS / family | Required evidence |
605+
|---|---|---|
606+
| `lab2` | Ubuntu 24.04 / DEB | DEB execution path: at least one `RecordedPrior` case |
607+
| `lab4` | AlmaLinux 9 / RPM | RPM execution path: at least one `RecordedPrior` case |
608+
| (TBD) | panel host (cPanel / DA / Plesk) | At least one `PanelNative` case if in scope at code phase |
609+
610+
Evidence plan is code-phase work; this section names the hosts only.
611+
612+
## 29. Reviewer checklist (merge-blocking, PR-25 code-phase only)
613+
614+
- [ ] §18.1 type definition matches verbatim; no exported mutators.
615+
- [ ] §18.2 construction paths exhaustive; no fourth path exists.
616+
- [ ] §18.3 payload invariants enforced in constructors.
617+
- [ ] §18.4 default-branch panic present in every `Kind` switch.
618+
- [ ] §19.2 layer 1 — execution terminals are NEW constants, not aliases.
619+
- [ ] §19.2 layer 4 — `main.go:132` writeHistory gate untouched.
620+
- [ ] §19.4 — new exit codes distinct from existing 0/4/5/6.
621+
- [ ] §20.1 mapping is static (not config-driven, not runtime-discovered).
622+
- [ ] §20.3 — no fallback paths exist anywhere in execution code.
623+
- [ ] §21.1 inline verification covers exactly the three assertions; nothing more.
624+
- [ ] §21.3 hard invariant — no code path removes safety net before §21.1 returns success.
625+
- [ ] §23 execution sequence respected exactly; no reordering.
626+
- [ ] §25 forbidden behaviors all absent from code.
627+
- [ ] §28 real-host evidence captured for at least lab2 + lab4.
628+
- [ ] CI gate updated minimally (no scope expansion beyond restore-execution surface).
629+
630+
---
631+
335632
## Amendment history
336633

337634
- **2026-04-20 v1 (seed)** — first committed seed. Lattice v2 + three locked corrections:
@@ -343,3 +640,5 @@ Four new gates in the `G4-RESTORE-*` namespace:
343640
2. `§6` Group 4 precedence clarifier added: 4.1 / 4.2 match on prior state for flags {`none`, `--restore`}; 4.3 matches `--panel-auto-takeover` regardless of prior.
344641

345642
Neither edit changes lattice behavior (§5 precedence already produces the correct outcome).
643+
644+
- **2026-04-27 v2 (PR-25 contract append)** — appends Part II (§§16–29: PR-25 execution contract). Faithful normalization of the locked Q1–Q5 design decisions (recorded 2026-04-20 during PR-24 freeze Day 0) following the locked "no expansion beyond Q1–Q5" rule. Sections §1–§15 are untouched. Verified live-code anchors at `internal/installer/uninstall/prior.go:278-284` (knownFirewallType set), `cmd/nftban-installer/main.go:132` (writeHistory gate), `internal/installer/state/machine.go:149-155` (existing exit-code constants). Doc-only commit; no code changes in this PR. Code phase opens in a separate PR after this one merges.

0 commit comments

Comments
 (0)