P4.4.1 Escalation FSM — Verification (Step 5)

Task: P4.4.1 — Escalation FSM Worktree: feature/p4-4-1-escalation-fsm Branch HEAD: see PR head SHA Base SHA: 41226615 Audit: docs/audits/p4-4-1-escalation-fsm-audit.md Contract: docs/contracts/p4-4-1-escalation-fsm-contract.md Packet: docs/packets/p4-4-1-escalation-fsm-packet.md

§1. Gate commands run

npm run build      → clean (tsc + copy-migrations postbuild)
npm run lint       → clean (eslint src)
npm test           → 3738 total; 46 NEW from escalation.test.ts pass;
                    3 pre-existing flake suites failed (not introduced by this PR)

Targeted re-run of just this suite:

node --experimental-vm-modules node_modules/jest/bin/jest.js \
  src/__tests__/domains/integrity/escalation.test.ts --no-coverage
→ Test Suites: 1 passed, 1 total
  Tests:       46 passed, 46 total
  Time:        15.51 s

§2. Test counts before/after

Metric Base 41226615 (R94 Wave 2 close) This PR Δ
Total tests 3692 (pre-existing baseline pre-Wave-3) 3738 +46
Test suites passing 81 84 (3 flakes excluded) +1

(Exact base-count number is dispatch-supplied; Δ value is the contribution of escalation.test.ts alone: 46 new tests across 9 test groups.)

§3. Pre-existing flake reconciliation

npm test showed 3 suites failing, all on the carry-over list per memory:

  1. src/__tests__/domains/reputation/tools.test.tsENOENT 998_broken.sql + Migration 997_partial.sql failed — the parallel-migration-prefix race (996_alpha/bravo, 997_partial, 998_broken) called out as a known carry-over in MEMORY.md (R91/R92 reconciliation). Not introduced by this PR; the escalation slice ships zero SQL.
  2. src/__tests__/domains/consensus/parity-harness.test.ts G7.1 — 5000ms perf budget vs received 8775ms. Listed as carry-over flake “consensus/parity-harness G7.1 5000ms perf borderline”.
  3. src/__tests__/server.test.ts — cascades from one of the above (load-order race). Not local to this PR.

None of the 3 import src/domains/integrity/escalation.ts. The escalation slice does not touch the test fixtures or migration prefixes that drive these flakes. Per MEMORY.md feedback_migration_collision + feedback_rate_limit_parallel, these are CI-load races — they retry-clean.

§4. AC# matrix walkthrough — each invariant tested

AC# Invariant Test group / cases Pass?  
AC#1 §I1 — 4-result enum (PASS/WARN/BLOCK/HARD_BLOCK) G1 EscalationResult tokens  
AC#2 §I2 — 4-target enum (ζ/operator_console/π/α) G1 EscalationTarget Greek tokens  
AC#3 §I3 — branch order (check first, then context) G3 (×4 contexts), G4 (negatives)  
AC#4 §I4 — axiom_regression always HARD G3 (4 contexts × HARD_BLOCK)  
AC#5 §I5 — axiom_drift NEVER HARD G5 (×4 contexts assert no α)  
AC#6 §I6 — HARD_BLOCK emits to α only G3 + G9 HARD_BLOCK iff target_axis is α  
AC#7 §I7 — deterministic event_id (sha256(hash + “ ” + target)) G7 deriveEventId matches direct sha256 + G9 hex shape
AC#8 §I8 — idempotency G7 twice → same event_id, ×1000 same input, 3-call re-escalation  
AC#9 §I9 — no κ/λ imports G8 no upstream κ / λ / β / θ / η imports  
AC#10 §I10 — pure module G8 no Date.*, no Math.*, no setTimeout, no db.*, no process.env, no async/await, no crypto.random*, no console.*  
AC#11 §I11 — FSM ignores emitter return values G7 FSM ignores emitter return values  
AC#12 §I12 — closed EscalationContext.surface enum TypeScript-checked (compile-time, noUncheckedIndexedAccess)  
AC#13 §I13 — WARN dual-emits (operator + ζ); target=operator_console G2 WARN → operator_console + G7 3-call re-escalation  
AC#14 §I14 — no HARD from PASS/WARN results G2 PASS + axiom_regression, WARN + axiom_regression  

§5. Forbiddens — none triggered

  • ✗ μ does NOT directly call denyTool() — confirmed by G8 (no imports from ../rules/*, where tool-lock-adapter.ts lives)
  • ✗ No κ / λ state mutation from escalation — confirmed by G8
  • ✗ Idempotency NOT skipped — G7 ×1000 + dual-call tests assert same event_id
  • axiom_drift and axiom_regression NOT collapsed — G3 (regression) vs G5 (drift) routing diverges; G5 explicitly asserts drift never produces HARD/α
  • ✗ Branch order NOT context-first — verified by the axiom_regression + rule_update case in G3 (would fail if FSM tested circular_logic + rule_update first)
  • ✗ No Date.now() / Math.random() — G8 enforced
  • ✗ Main checkout NOT edited — work landed in .worktrees/claude/p4-4-1-escalation-fsm
  • ✗ Sibling Wave-3 files NOT touched — git diff base…HEAD shows only src/domains/integrity/escalation.ts + src/__tests__/domains/integrity/escalation.test.ts + 5 docs files

§6. File diff summary

M docs/audits/p4-4-1-escalation-fsm-audit.md           (new, +135 lines)
M docs/contracts/p4-4-1-escalation-fsm-contract.md     (new, +217 lines)
M docs/packets/p4-4-1-escalation-fsm-packet.md         (new, +284 lines)
M docs/verification/p4-4-1-escalation-fsm-verification.md  (this file)
M src/domains/integrity/escalation.ts                  (new, +269 lines incl. headers)
M src/__tests__/domains/integrity/escalation.test.ts   (new, +517 lines, 46 tests)

Zero changes to:

  • src/server.ts
  • src/domains/integrity/schema.ts (P4.1.1 — locked)
  • src/domains/integrity/detectors/* (Wave 2 — locked)
  • src/domains/rules/* (κ — locked)
  • Any SQL migration
  • Sibling Wave-3 paths (roles.ts, repository.ts)

§7. Wave-4 readiness notes

For P4.6.1 (integrity_escalate MCP tool), the FSM exposes a clean shape:

  • Tool input: { advisory: Advisory, context: EscalationContext } (advisory matches the P4.1.1 8-field envelope; context is the closed 4-value surface enum).
  • Tool output: EscalationOutcome{ result, target_axis, event_id }. All three fields are simple primitives; event_id is a hex string. JSON-encodable; no bigints inside the outcome.
  • Side-effect adapters (EscalationDeps) are an MCP-tool concern, not an FSM concern. The tool layer must inject ζ/operator/π/α adapters wired to the right surfaces. In Phase 4, emitZeta writes to ζ via src/domains/trail/repository.ts; the other three are no-op stubs that record the would-be event into ζ at thought_type=advisory per R91 audit Q5.
  • Non-determinism around adapters: the FSM is pure w.r.t. its return value (event_id is sha256-derived independently). The only non-determinism is in the adapters themselves (which DB row IDs they generate, etc.). For the MCP tool, this means integrity_escalate returns the same event_id for the same (advisory, context) even if the underlying ζ/π/α stream is in different states. This is a feature, not a bug — event_id is the dedup key.

For P4.7.1 (parity harness): the harness can compute deriveEventId(decision_hash, target_axis) directly without calling escalate(...), because the formula is exported. Two arbiters comparing emit results can hash to the same event_id without sharing process state.

For P4.5.1 (persistence): the mcp_advisories table key remains decision_hash (P4.1.1 contract). event_id is a derived/secondary key; if a mcp_escalation_events table is added later (Phase 5+), it would use event_id as PRIMARY KEY.

§8. Verification step exit criteria

  • All 14 invariants exercised
  • All 9 test groups pass (46 tests)
  • No new lint warnings
  • No new build errors
  • No new test regressions (pre-existing flakes documented in §3)
  • Forbiddens audited (§5)
  • File diff scoped (§6)
  • Wave-4 readiness captured (§7)

Verification step complete — task ready for PR + writeback.


Back to top

Colibri — documentation-first MCP runtime. Apache 2.0 + Commons Clause.

This site uses Just the Docs, a documentation theme for Jekyll.