P4.4.1 Escalation FSM — Execution Packet (Step 3)
Task: P4.4.1 — Escalation FSM
Worktree: feature/p4-4-1-escalation-fsm
Audit: docs/audits/p4-4-1-escalation-fsm-audit.md
Contract: docs/contracts/p4-4-1-escalation-fsm-contract.md
Base SHA: 41226615
§1. Files
Create
| Path | Purpose |
|---|---|
src/domains/integrity/escalation.ts |
The FSM module — types + escalate() + TARGET_FIELD_SEPARATOR + deriveEventId() |
src/__tests__/domains/integrity/escalation.test.ts |
All 7 test groups (G1–G7) |
docs/audits/p4-4-1-escalation-fsm-audit.md |
Step 1 (already committed) |
docs/contracts/p4-4-1-escalation-fsm-contract.md |
Step 2 (already committed) |
docs/packets/p4-4-1-escalation-fsm-packet.md |
Step 3 (this file) |
docs/verification/p4-4-1-escalation-fsm-verification.md |
Step 5 |
Do NOT touch
src/domains/integrity/schema.ts(P4.1.1 — locked)src/domains/integrity/detectors/{circular,coercion,drift}.ts(P4.2.* — Wave 2 locked)src/domains/rules/tool-lock-adapter.ts(P1.4.4 — locked)src/server.ts(no MCP registration — P4.6.1’s job)- Sibling Wave 3 paths (
roles.tsfrom P4.3.1;repository.ts+ migration SQL from P4.5.1)
§2. Module structure
// src/domains/integrity/escalation.ts
/**
* Colibri — Phase 4 μ Integrity Monitor — Escalation FSM (P4.4.1).
*
* <multi-paragraph header — see §3 below>
*/
import { createHash } from 'node:crypto';
import type { Advisory } from './schema.js';
// §1. Type surface (5 types)
export type EscalationResult = 'PASS' | 'WARN' | 'BLOCK' | 'HARD_BLOCK';
export type EscalationTarget = 'ζ' | 'operator_console' | 'π' | 'α';
export type EscalationContext = {
readonly surface: 'rule_update' | 'admission_gate' | 'governance_intake' | 'other';
};
export type EscalationOutcome = {
readonly result: EscalationResult;
readonly target_axis: EscalationTarget;
readonly event_id: string;
};
export type EscalationDeps = {
emitZeta(advisory: Advisory): string;
emitOperator(advisory: Advisory): string;
emitPi(advisory: Advisory): string;
emitAlpha(advisory: Advisory): string;
};
// §2. Constant
export const TARGET_FIELD_SEPARATOR = '|';
// §3. Helper (exported for the verification harness; consumers should use `escalate`)
export function deriveEventId(decision_hash: string, target_axis: EscalationTarget): string {
const preimage = decision_hash + TARGET_FIELD_SEPARATOR + target_axis;
return createHash('sha256').update(preimage, 'utf8').digest('hex');
}
// §4. The FSM
export function escalate(
advisory: Advisory,
context: EscalationContext,
deps: EscalationDeps,
): EscalationOutcome {
switch (advisory.result) {
case 'PASS': {
deps.emitZeta(advisory);
return {
result: 'PASS',
target_axis: 'ζ',
event_id: deriveEventId(advisory.decision_hash, 'ζ'),
};
}
case 'WARN': {
deps.emitOperator(advisory);
deps.emitZeta(advisory);
return {
result: 'WARN',
target_axis: 'operator_console',
event_id: deriveEventId(advisory.decision_hash, 'operator_console'),
};
}
case 'BLOCK': {
// Branch on check FIRST so axiom_regression's always-HARD invariant dominates.
if (advisory.check === 'axiom_regression') {
deps.emitAlpha(advisory);
return {
result: 'HARD_BLOCK',
target_axis: 'α',
event_id: deriveEventId(advisory.decision_hash, 'α'),
};
}
if (advisory.check === 'circular_logic' && context.surface === 'rule_update') {
deps.emitAlpha(advisory);
return {
result: 'HARD_BLOCK',
target_axis: 'α',
event_id: deriveEventId(advisory.decision_hash, 'α'),
};
}
if (advisory.check === 'coercion_trap' && context.surface === 'admission_gate') {
deps.emitAlpha(advisory);
return {
result: 'HARD_BLOCK',
target_axis: 'α',
event_id: deriveEventId(advisory.decision_hash, 'α'),
};
}
if (advisory.check === 'axiom_drift' && context.surface === 'governance_intake') {
deps.emitPi(advisory);
return {
result: 'BLOCK',
target_axis: 'π',
event_id: deriveEventId(advisory.decision_hash, 'π'),
};
}
// Default BLOCK route — π intake.
deps.emitPi(advisory);
return {
result: 'BLOCK',
target_axis: 'π',
event_id: deriveEventId(advisory.decision_hash, 'π'),
};
}
}
}
The TypeScript exhaustiveness checker handles the missing-default for switch(advisory.result) since AdvisoryResult is a closed 3-value enum (PASS/WARN/BLOCK). If we miss a case, tsc errors. No explicit default: needed.
§3. Module header
Multi-paragraph doc comment covering:
- Greek letter μ + Phase 4 + Wave 3 + P4.4.1.
- Public surface (5 types +
TARGET_FIELD_SEPARATORconstant +deriveEventIdhelper +escalatefunction). - The 4-result FSM behaviour.
- The 3 invariant mappings (circular+rule_update, coercion+admission, drift+governance).
- The
axiom_regression-always-HARD rule. - Determinism guarantee (no Date/Math/random; same input → same
event_id). - The “FSM derives event_id, ignores emitter returns” choice (§I11 of contract).
- References to: audit, contract, integrity.md L148-157, s14, P4.1.1 schema, Wave 2 detectors, P1.4.4 tool-lock-adapter.
§4. Test plan (src/__tests__/domains/integrity/escalation.test.ts)
§G1 — Enum shape (compile-time + runtime)
- Assert the 4 result tokens exist as TS literal-union members (compile-time via
satisfies). - Assert the 4 target tokens exist (compile-time).
- Runtime: build outcomes for each result; check
outcome.resultandoutcome.target_axisare the expected literal strings.
§G2 — PASS / WARN paths
- PASS path: advisory with
result=PASS;escalate(advisory, ctx, mocks)→ outcome{ result: 'PASS', target_axis: 'ζ', event_id: <hex> }. Mocks: assertemitZetacalled once;emitOperator/emitPi/emitAlphaNOT called. - WARN path: advisory with
result=WARN; outcome{ result: 'WARN', target_axis: 'operator_console', event_id: <hex> }. Mocks: assertemitOperatorcalled once,emitZetacalled once;emitPi/emitAlphaNOT called. - PASS + axiom_regression check: result stays PASS; emit only to ζ. (Closes §I14.)
- WARN + axiom_regression check: result stays WARN; emit only to operator + ζ.
§G3 — axiom_regression always HARD
- Iterate context.surface across
['rule_update', 'admission_gate', 'governance_intake', 'other']. - For each: advisory with
result=BLOCK, check=axiom_regression→ outcome HARD_BLOCK / α. - Mocks:
emitAlphacalled once per case;emitPi/emitOperator/emitZetaNOT called.
§G4 — Invariant mappings 1 + 2
- Mapping 1:
BLOCK + circular_logic + rule_update→ HARD_BLOCK / α;emitAlphacalled. - Mapping 1 negative:
BLOCK + circular_logic + admission_gate→ BLOCK / π;emitPicalled. - Mapping 2:
BLOCK + coercion_trap + admission_gate→ HARD_BLOCK / α;emitAlphacalled. - Mapping 2 negative:
BLOCK + coercion_trap + rule_update→ BLOCK / π;emitPicalled.
§G5 — Mapping 3 + axiom_drift distinct routing
- Mapping 3:
BLOCK + axiom_drift + governance_intake→ BLOCK / π;emitPicalled. - axiom_drift other:
BLOCK + axiom_drift + admission_gate→ BLOCK / π;emitPicalled. - axiom_drift other 2:
BLOCK + axiom_drift + other→ BLOCK / π;emitPicalled. - Distinct from axiom_regression: confirm none of the axiom_drift cases ever return HARD_BLOCK or call
emitAlpha. (Closes §I5.)
§G6 — Default BLOCK route
BLOCK + circular_logic + other→ BLOCK / π.BLOCK + coercion_trap + other→ BLOCK / π.BLOCK + circular_logic + governance_intake→ BLOCK / π.- All call
emitPi; none callemitAlpha.
§G7 — Determinism + idempotency
- Build an advisory; call
escalate(advisory, ctx, mocks)twice — assert returnedevent_idis identical both times. - Call across 1000 iterations with fresh mocks each time — assert all
event_ids for a fixed input are identical. - Two distinct advisories (different
decision_hash) → differentevent_ids. deriveEventId('a'.repeat(64), 'α')returns a string matching/^[a-f0-9]{64}$/.- Emitters return arbitrary garbage strings (
'EMITTER-RETURN-IGNORED') — assertoutcome.event_idis NOT that string. (Closes §I11.)
§G8 — Static scanner
- Read
src/domains/integrity/escalation.tssource. Assert ZERO matches for:\bDate\b(no Date.* usage)\bMath\b(no Math.* usage)\bsetTimeout\b,\bsetInterval\b\bdb\.run\b,\bdb\.exec\b,\bdb\.prepare\b\bprocess\.env\b\basync\s+function\b,\basync\s+\(,\bawait\b\bcrypto\.randomBytes\b,\bcrypto\.randomUUID\b
- Allowed:
createHash(named import fromnode:crypto). - Allowed:
import type { Advisory }from./schema.js. - Forbid imports from
../rules/*(no κ state read). - Forbid imports from
../reputation/*(no λ state read). - Forbid imports from
../tasks/*,../trail/*,../proof/*,../consensus/*.
§G9 — Type narrowness
- Compile-time:
escalatereturnsEscalationOutcome(TS-checked). - Runtime: assert
outcome.event_id.length === 64for every test case. - Runtime: assert
outcome.target_axisis one of the 4 enum values.
§5. Wave-3 collision safety
P4.3.1 (advisory roles) operates on src/domains/integrity/roles.ts + its test. P4.5.1 (advisory persistence) operates on src/domains/integrity/repository.ts + a migration SQL file (e.g. src/db/migrations/NNN_advisories.sql) + its test.
My scope:
src/domains/integrity/escalation.ts(new file — no collision)src/__tests__/domains/integrity/escalation.test.ts(new file — no collision)docs/audits|contracts|packets|verification/p4-4-1-*.md(new files — no collision)
No shared file with any Wave 3 sibling. No shared SQL migration prefix (my slice ships zero SQL).
§6. Build / lint / test gate
After implementation (Step 4):
cd E:/AMS/.worktrees/claude/p4-4-1-escalation-fsm
npm run build && npm run lint && npm test
All three are GATES (per CLAUDE.md §5).
Expected: tests grow from base count by 7 groups × ~3 cases each ≈ 20-30 new test cases. Build clean (TS strict). Lint clean (no any, no unused imports, etc.).
§7. Writeback
Push branch, open PR. PR body carries:
task_id: p4-4-1-escalation-fsm-r94-w3
branch: feature/p4-4-1-escalation-fsm
worktree: .worktrees/claude/p4-4-1-escalation-fsm
commit: <SHA of HEAD>
tests: npm run build && npm run lint && npm test (<X>/<Y> pass; +Δ)
summary: ... (per dispatch packet)
blockers: none (or list)
§8. Step exit criteria
- File list locked
- Module structure scaffolded
- Header content sketched
- 9 test groups defined with concrete assertions
- Wave-3 collision safety confirmed (zero shared paths)
- Gate command identified
Packet step complete — proceeding to §Step 4 implement.