P4.4.1 Escalation FSM — Audit (Step 1)
Task: P4.4.1 — Escalation FSM (4-result + 3 invariant mappings)
Worktree: feature/p4-4-1-escalation-fsm
Base SHA: 41226615
Greek: μ (mu) Integrity Monitor, Phase 4 Wave 3
Author: T3 executor
§1. Scope
Translate a P4.1.1 Advisory into an enforcement-axis target:
escalate(advisory: Advisory, context: EscalationContext, deps: EscalationDeps): EscalationOutcome
Where:
Advisory.result ∈ {PASS, WARN, BLOCK}is the inputEscalationResult ∈ {PASS, WARN, BLOCK, HARD_BLOCK}is the output (BLOCK splits into BLOCK/HARD_BLOCK on three invariant rules + theaxiom_regressionalways-HARD rule)target_axis ∈ {ζ, operator_console, π, α}is the emitter that firesevent_idis deterministic fromdecision_hash + target_axis
μ never directly denies — it emits typed events to α/π/ζ adapters injected by the caller.
§2. Inputs surveyed
§2.1 Advisory envelope (P4.1.1)
Defined at src/domains/integrity/schema.ts §4. The 8-field shape is locked:
{
role: 'Translator' | 'Sentinel' | 'Guide',
check: 'circular_logic' | 'coercion_trap' | 'axiom_drift' | 'axiom_regression',
result: 'PASS' | 'WARN' | 'BLOCK',
severity: 'LOW' | 'MED' | 'HIGH',
evidence: unknown[],
recommendation: string,
decision_hash: /^[a-f0-9]{64}$/,
timestamp_logical: bigint, // non-negative
}
Key for escalation:
resultdrives the FSM (PASS / WARN / BLOCK base path)checkdiscriminates the invariant mappings within BLOCKdecision_hashis the deterministic identity key (already canonicalized; we just deriveevent_idfrom it)
AdvisoryCheckSchema is a closed enum of 4 — axiom_drift and axiom_regression are distinct check values (per Wave 2 finding 5). Routing must preserve the distinction.
§2.2 Wave 2 detector outputs
Wave 2 (PRs landed at 41226615) shipped 3 detectors:
| File | Check produced | result | severity | role |
|---|---|---|---|---|
src/domains/integrity/detectors/circular.ts |
circular_logic |
WARN |
HIGH |
Sentinel |
src/domains/integrity/detectors/coercion.ts |
coercion_trap |
(per detector) | (per detector) | Sentinel |
src/domains/integrity/detectors/drift.ts |
axiom_drift or axiom_regression |
(per detector) | (per detector) | Sentinel |
The escalation FSM consumes the typed Advisory regardless of which detector produced it. Detectors are not imported by this slice — only the type.
§2.3 Escalation mapping doc (integrity.md L148-157)
| Result | Action |
| PASS | Logged to ζ at thought_type=advisory; no further effect |
| WARN | Logged; surfaced in the operator console; no rule change |
| BLOCK | Denies the proposal at π intake (pre-ENACTED) |
| HARD BLOCK | Denies at α's tool-lock admission (s10); downstream κ never runs |
R91-staged dispatch carries the further refinement: HARD BLOCK is encoded as result=BLOCK + severity=HIGH at the advisory level (P4.1.1 §4, AdvisoryResultSchema rejects a 4-value enum), and the FSM raises BLOCK to HARD_BLOCK via the three invariant mappings + the always-HARD axiom_regression rule.
§2.4 tool-lock-adapter API (P1.4.4 — α consumer)
src/domains/rules/tool-lock-adapter.ts exposes createToolLockAdapter(registry, options?) returning a MiddlewareStage. The adapter consumes admission events; it does NOT today read from a μ-emitted event stream — that wiring is a downstream Phase 5+ task. For P4.4.1 we treat α as a black-box emitter: deps.emitAlpha(advisory) returns the deterministic event_id; the caller is responsible for piping that event into α at integration time.
§2.5 Determinism corpus
src/domains/integrity/schema.ts and the Wave 2 detectors all pass a determinism self-scan (no Date.*, no Math.random, no crypto.randomBytes, no async/await with locale-dependent ordering). The escalation module inherits the same posture: pure function, deterministic event_id from decision_hash + target_axis.
§3. Outputs of this slice
| File | Role |
|---|---|
src/domains/integrity/escalation.ts |
The FSM + type surface |
src/__tests__/domains/integrity/escalation.test.ts |
All result paths + invariant mappings + idempotency + static scanner |
No SQL migration, no detector edits, no MCP-tools-table change (P4.6.1’s job).
§4. Risks / known hazards
-
Branch-order trap. If the FSM branches on
context.surfacebeforeadvisory.check, theaxiom_regression-always-HARD invariant breaks (arule_updatecontext could be checked against thecircular_logic + rule_updatemapping whileaxiom_regressionslips through unmapped). Mitigation: branch oncheckfirst, then context (per dispatch packet common-gotcha L1219 + integrity.md L112). -
HARD_BLOCK semantics divergence. P4.1.1’s
AdvisoryResultSchemais[PASS, WARN, BLOCK](3-value enum). The escalation FSMEscalationResultis[PASS, WARN, BLOCK, HARD_BLOCK](4-value enum). These are NOT interchangeable: input is 3-valued, output is 4-valued. The FSM is the layer that introduces HARD_BLOCK. -
Idempotency contract. Same
(advisory, context)MUST produce sameevent_id. We pick the strict contract: the FSM itself derivesevent_idassha256(decision_hash || target_axis)(or simpler — exact form fixed in contract). This means the FSM does not depend on the emitter adapters to return deterministic IDs — they may return any string; we ignore their return value when computingevent_id. Test mocks may return constants for emit-side-effect assertions. -
axiom_driftvsaxiom_regressioncollapsing. They are distinct check values (per Wave 2 finding 5). The FSM must not collapse them.axiom_regressionis ALWAYS HARD_BLOCK;axiom_driftis BLOCK by default and HARD only via invariant mapping 3 (which is BLOCK via π, NOT HARD_BLOCK per the spec — re-read integrity.md L150-157, line 154: “BLOCK → π intake”). So mapping 3 is actually BLOCK + target_axis=π, not HARD_BLOCK. The dispatch spec is explicit at L1132-1133. - Emitter return values. The spec’s
EscalationDeps.emitXtyped signatures allreturn string. But the FSM contract saysevent_idis deterministic fromdecision_hash + target_axis. Two paths:- (A) FSM computes
event_id, ignores emitter return value. - (B) Emitter returns
event_id; FSM trusts it. We pick (A) — the FSM is the authority forevent_idshape. Emitters are side-effect handles, not identity sources. The contract documents this and the tests assert it.
- (A) FSM computes
-
WARN dual-emit. WARN calls BOTH
emitOperatorANDemitZeta(spec line 1123). The FSM picks onetarget_axisfor the outcome — by spec it’soperator_console. ζ is the secondary log surface but doesn’t drive routing. Test asserts both emitters fire; outcome.target_axis isoperator_console. - Test layout inconsistency. Wave 2 detectors used TWO paths:
circular.test.tsandcoercion.test.tslive atsrc/domains/integrity/detectors/__tests__/;drift.test.tsand the P4.1.1schema.test.tslive atsrc/__tests__/domains/integrity/{detectors/}*. The dispatch packet picks the latter —src/__tests__/domains/integrity/escalation.test.ts. We follow the dispatch packet.
§5. Pre-existing baseline
- Tests at base: PR #274 (Wave 2 close, P4.2.1) — verification doc at the worktree-relative path will reveal the count.
- Lint at base: clean (Wave 2 enforced determinism corpus).
- Build at base: clean.
§6. References
docs/3-world/physics/enforcement/integrity.md§Escalation mapping (L148-157), §axiom_regression (L112)docs/spec/s14-integrity-monitor.md§When advisory becomes enforcementdocs/guides/implementation/task-prompts/p4.1-mu-integrity.md§P4.4.1 (L1034-1222)src/domains/integrity/schema.ts§4 —Advisoryenvelope (P4.1.1)src/domains/integrity/detectors/{circular,coercion,drift}.ts— Wave 2 producerssrc/domains/rules/tool-lock-adapter.ts— P1.4.4 α consumer (downstream ofemitAlpha)
§7. Audit-step exit criteria
- Confirmed
Advisoryshape andAdvisoryCheck4-value enum - Located escalation mapping doc lines (L148-157)
- Identified α / π / ζ / operator_console as the four emitter axes
- Resolved
event_idauthority (FSM, not emitters) - Resolved
target_axisfor WARN (operator_console; ζ is dual-emit secondary) - Captured the 7 risks above for the contract step
Audit step complete — proceeding to §Step 2 contract.