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 input
  • EscalationResult ∈ {PASS, WARN, BLOCK, HARD_BLOCK} is the output (BLOCK splits into BLOCK/HARD_BLOCK on three invariant rules + the axiom_regression always-HARD rule)
  • target_axis ∈ {ζ, operator_console, π, α} is the emitter that fires
  • event_id is deterministic from decision_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:

  • result drives the FSM (PASS / WARN / BLOCK base path)
  • check discriminates the invariant mappings within BLOCK
  • decision_hash is the deterministic identity key (already canonicalized; we just derive event_id from 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

  1. Branch-order trap. If the FSM branches on context.surface before advisory.check, the axiom_regression-always-HARD invariant breaks (a rule_update context could be checked against the circular_logic + rule_update mapping while axiom_regression slips through unmapped). Mitigation: branch on check first, then context (per dispatch packet common-gotcha L1219 + integrity.md L112).

  2. HARD_BLOCK semantics divergence. P4.1.1’s AdvisoryResultSchema is [PASS, WARN, BLOCK] (3-value enum). The escalation FSM EscalationResult is [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.

  3. Idempotency contract. Same (advisory, context) MUST produce same event_id. We pick the strict contract: the FSM itself derives event_id as sha256(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 computing event_id. Test mocks may return constants for emit-side-effect assertions.

  4. axiom_drift vs axiom_regression collapsing. They are distinct check values (per Wave 2 finding 5). The FSM must not collapse them. axiom_regression is ALWAYS HARD_BLOCK; axiom_drift is 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.

  5. Emitter return values. The spec’s EscalationDeps.emitX typed signatures all return string. But the FSM contract says event_id is deterministic from decision_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 for event_id shape. Emitters are side-effect handles, not identity sources. The contract documents this and the tests assert it.
  6. WARN dual-emit. WARN calls BOTH emitOperator AND emitZeta (spec line 1123). The FSM picks one target_axis for the outcome — by spec it’s operator_console. ζ is the secondary log surface but doesn’t drive routing. Test asserts both emitters fire; outcome.target_axis is operator_console.

  7. Test layout inconsistency. Wave 2 detectors used TWO paths: circular.test.ts and coercion.test.ts live at src/domains/integrity/detectors/__tests__/; drift.test.ts and the P4.1.1 schema.test.ts live at src/__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 enforcement
  • docs/guides/implementation/task-prompts/p4.1-mu-integrity.md §P4.4.1 (L1034-1222)
  • src/domains/integrity/schema.ts §4 — Advisory envelope (P4.1.1)
  • src/domains/integrity/detectors/{circular,coercion,drift}.ts — Wave 2 producers
  • src/domains/rules/tool-lock-adapter.ts — P1.4.4 α consumer (downstream of emitAlpha)

§7. Audit-step exit criteria

  • Confirmed Advisory shape and AdvisoryCheck 4-value enum
  • Located escalation mapping doc lines (L148-157)
  • Identified α / π / ζ / operator_console as the four emitter axes
  • Resolved event_id authority (FSM, not emitters)
  • Resolved target_axis for WARN (operator_console; ζ is dual-emit secondary)
  • Captured the 7 risks above for the contract step

Audit step complete — proceeding to §Step 2 contract.


Back to top

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

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