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.ts from 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:

  1. Greek letter μ + Phase 4 + Wave 3 + P4.4.1.
  2. Public surface (5 types + TARGET_FIELD_SEPARATOR constant + deriveEventId helper + escalate function).
  3. The 4-result FSM behaviour.
  4. The 3 invariant mappings (circular+rule_update, coercion+admission, drift+governance).
  5. The axiom_regression-always-HARD rule.
  6. Determinism guarantee (no Date/Math/random; same input → same event_id).
  7. The “FSM derives event_id, ignores emitter returns” choice (§I11 of contract).
  8. 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.result and outcome.target_axis are 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: assert emitZeta called once; emitOperator/emitPi/emitAlpha NOT called.
  • WARN path: advisory with result=WARN; outcome { result: 'WARN', target_axis: 'operator_console', event_id: <hex> }. Mocks: assert emitOperator called once, emitZeta called once; emitPi/emitAlpha NOT 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: emitAlpha called once per case; emitPi/emitOperator/emitZeta NOT called.

§G4 — Invariant mappings 1 + 2

  • Mapping 1: BLOCK + circular_logic + rule_update → HARD_BLOCK / α; emitAlpha called.
  • Mapping 1 negative: BLOCK + circular_logic + admission_gate → BLOCK / π; emitPi called.
  • Mapping 2: BLOCK + coercion_trap + admission_gate → HARD_BLOCK / α; emitAlpha called.
  • Mapping 2 negative: BLOCK + coercion_trap + rule_update → BLOCK / π; emitPi called.

§G5 — Mapping 3 + axiom_drift distinct routing

  • Mapping 3: BLOCK + axiom_drift + governance_intake → BLOCK / π; emitPi called.
  • axiom_drift other: BLOCK + axiom_drift + admission_gate → BLOCK / π; emitPi called.
  • axiom_drift other 2: BLOCK + axiom_drift + other → BLOCK / π; emitPi called.
  • 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 call emitAlpha.

§G7 — Determinism + idempotency

  • Build an advisory; call escalate(advisory, ctx, mocks) twice — assert returned event_id is 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) → different event_ids.
  • deriveEventId('a'.repeat(64), 'α') returns a string matching /^[a-f0-9]{64}$/.
  • Emitters return arbitrary garbage strings ('EMITTER-RETURN-IGNORED') — assert outcome.event_id is NOT that string. (Closes §I11.)

§G8 — Static scanner

  • Read src/domains/integrity/escalation.ts source. 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 from node: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: escalate returns EscalationOutcome (TS-checked).
  • Runtime: assert outcome.event_id.length === 64 for every test case.
  • Runtime: assert outcome.target_axis is 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.


Back to top

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

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