/**
 * AEGIS SOAR (AS/PLATFORM/2026/002) — M2 INVESTIGATION BATTERY, HARNESS-BACKED CLONE.
 *
 * This is the proven-equivalence ARTEFACT (spec §2): a faithful re-author of
 * scripts/fincrime-m2-investigation-route-battery.ts that drives the SAME 24 legs
 * through the extracted proof-harness toolkit (scripts/lib/proof-harness.ts) instead
 * of re-deriving the scaffolding inline. If this reproduces the original's 24/24 —
 * each leg passing for its ORIGINAL reason — the harness is proven correct.
 *
 * It reproduces the original's EXACT recorded leg ids (incl. the id="10" used for the
 * final no-silent-close global leg, whose comment said "LEG 13" — reproduced verbatim
 * so the equivalence diff is byte-clean), and emits per-leg STRUCTURED `signals` (the
 * discriminating observables) so the equivalence gate can prove "same reason", not
 * merely "same pass boolean".
 *
 * Distinct seed prefix (fc-m2h) + per-run rotated RFC-5737 IPs so it never collides
 * with the original battery if both run in the same window.
 *
 * Run:  PROOF_JSON_OUT=scripts/.artifacts/m2-harness-results.json tsx scripts/fincrime-m2-investigation-route-battery-harness.ts
 */

import { sql } from "drizzle-orm";
import {
  ProofRun,
  verdict,
  Jar,
  Session,
  getCsrf,
  login,
  call,
  sessionClient,
  allocateActorIps,
  mkSeed,
  seedUser,
  deactivateUsers,
  deleteByColIn,
  leakProbe,
  LeakSurface,
  heldLockProof,
  rlsCountById,
  ownerPool,
  ownerQuery,
  endOwnerPool,
  sleep,
  randomUUID,
  createHash,
  withTenantRls,
  type Seed,
} from "./lib/proof-harness";

const TENANT_A = "aegis-sovereign";
const TENANT_B = "stanbic-ug-001";
const base = Date.now();

const [RM_XFF, ADMIN_XFF, AUD_XFF, ANON_XFF] = allocateActorIps(4, base);

const RAW_NOTE = `RAWNOTE-DO-NOT-LEAK-${base}`;
const RAW_SUMMARY = `RAWSUMMARY-DO-NOT-LEAK-${base}`;

const run = new ProofRun("AEGIS SOAR M2 Investigation — HARNESS-BACKED equivalence clone", "DEV");
const createdCaseIds = new Set<string>();

// ── fincrime-typed ground-truth read-backs (composed from harness ownerQuery) ─
interface CaseRow {
  id: string;
  tenant_id: string;
  state: string;
  assigned_investigator_id: string | null;
  disposition: string | null;
  disposition_reason: string | null;
  dispositioned_at: Date | null;
  str_filed: boolean;
}
const caseRow = async (t: string, id: string): Promise<CaseRow | undefined> =>
  (
    await ownerQuery<CaseRow>(
      "SELECT id, tenant_id, state, assigned_investigator_id, disposition, disposition_reason, dispositioned_at, str_filed FROM fincrime_cases WHERE tenant_id=$1 AND id=$2",
      [t, id],
    )
  )[0];

interface EventRow {
  event_type: string;
  from_state: string | null;
  to_state: string | null;
  event_payload: string;
  payload_hash: string;
  audit_chain_entry_id: number | null;
}
const eventsFor = (t: string, caseId: string): Promise<EventRow[]> =>
  ownerQuery<EventRow>(
    "SELECT event_type, from_state, to_state, event_payload, payload_hash, audit_chain_entry_id FROM fincrime_case_events WHERE tenant_id=$1 AND case_id=$2 ORDER BY created_at",
    [t, caseId],
  );

interface EvidenceRow {
  evidence_type: string;
  opaque_ref: string | null;
  encrypted_note: string | null;
  payload_hash: string;
  audit_chain_entry_id: number | null;
}
const evidenceFor = (t: string, caseId: string): Promise<EvidenceRow[]> =>
  ownerQuery<EvidenceRow>(
    "SELECT evidence_type, opaque_ref, encrypted_note, payload_hash, audit_chain_entry_id FROM fincrime_case_evidence WHERE tenant_id=$1 AND case_id=$2 ORDER BY created_at",
    [t, caseId],
  );

const attemptCount = async (action: string, caseId: string): Promise<number> =>
  Number(
    (
      await ownerQuery<{ n: number }>(
        "SELECT count(*)::int AS n FROM audit_chain_entries WHERE action=$1 AND resource=$2",
        [action, `fincrime_case:${caseId}`],
      )
    )[0]?.n ?? 0,
  );

const rlsVisible = (t: string, id: string): Promise<number> => rlsCountById(t, "fincrime_cases", id);

const dispositionedAmong = async (ids: string[]): Promise<number> =>
  !ids.length
    ? 0
    : Number(
        (
          await ownerQuery<{ n: number }>(
            "SELECT count(*)::int AS n FROM fincrime_cases WHERE id = ANY($1::varchar[]) AND state = 'DISPOSITIONED'",
            [ids],
          )
        )[0]?.n ?? 0,
      );

const withDispositionAmong = async (ids: string[]): Promise<number> =>
  !ids.length
    ? 0
    : Number(
        (
          await ownerQuery<{ n: number }>(
            "SELECT count(*)::int AS n FROM fincrime_cases WHERE id = ANY($1::varchar[]) AND (disposition IS NOT NULL OR disposition_reason IS NOT NULL OR dispositioned_at IS NOT NULL OR str_filed = true)",
            [ids],
          )
        )[0]?.n ?? 0,
      );

// STR-report owner read-back: a well-formed STR probe rejected at a STATE precondition
// must have created NO fincrime_str_reports row (the write never reached the insert).
const strReportCount = async (t: string, caseId: string): Promise<number> =>
  Number(
    (
      await ownerQuery<{ n: number }>(
        "SELECT count(*)::int AS n FROM fincrime_str_reports WHERE tenant_id=$1 AND case_id=$2",
        [t, caseId],
      )
    )[0]?.n ?? 0,
  );

const leakSurfaces = (caseId: string): LeakSurface[] => [
  { label: "evidence.encrypted_note", table: "fincrime_case_evidence", col: "encrypted_note", whereCol: "case_id", whereVal: caseId },
  { label: "events.event_payload", table: "fincrime_case_events", col: "event_payload", whereCol: "case_id", whereVal: caseId },
  { label: "audit.details", table: "audit_chain_entries", col: "details", whereCol: "resource", whereVal: `fincrime_case:${caseId}` },
];

// ── synthetic OPEN-case fixture ──────────────────────────────────────────────
async function seedOpenCase(tenantId: string, tag: string): Promise<string> {
  const id = randomUUID();
  await ownerPool().query(
    `INSERT INTO fincrime_cases (id, tenant_id, source_type, source_ref, source_signal_hash, subject_ref)
     VALUES ($1, $2, 'MANUAL_REFERRAL', $3, $4, $5)`,
    [
      id,
      tenantId,
      `ext-${tag}-${base}`,
      createHash("sha256").update(`${tag}-${base}-${randomUUID()}`).digest("hex"),
      `opaque:m2h-${tag}-${base}`,
    ],
  );
  createdCaseIds.add(id);
  return id;
}

// ── seeds (distinct prefix from the original's fc-m2) ─────────────────────────
const adminSeed = mkSeed("admin", "fc-m2h", "admin", base);
const rmSeed = mkSeed("risk_manager", "fc-m2h", "rm", base);
const audSeed = mkSeed("auditor", "fc-m2h", "aud", base);
const rm2Seed = mkSeed("risk_manager", "fc-m2h", "rm2", base);
const bUserSeed = mkSeed("risk_manager", "fc-m2h", "buser", base);
const allSeeds: Seed[] = [adminSeed, rmSeed, audSeed, rm2Seed, bUserSeed];

async function main() {
  console.log("=".repeat(82));
  console.log("AEGIS SOAR M2 Investigation — HARNESS-BACKED battery (proven-equivalence clone)");
  console.log("spec AS/PLATFORM/2026/002 | env: DEV |", new Date().toISOString());
  console.log("=".repeat(82));

  try {
    await seedUser(adminSeed, TENANT_A, "FINCRIME M2H BATTERY admin (test, delete ok)");
    await seedUser(rmSeed, TENANT_A, "FINCRIME M2H BATTERY risk_manager (test, delete ok)");
    await seedUser(audSeed, TENANT_A, "FINCRIME M2H BATTERY auditor (test, delete ok)");
    await seedUser(rm2Seed, TENANT_A, "FINCRIME M2H BATTERY rm2 (test, delete ok)");
    await seedUser(bUserSeed, TENANT_B, "FINCRIME M2H BATTERY buser (test, delete ok)");

    const adminS = await login(adminSeed, ADMIN_XFF);
    const rmS = await login(rmSeed, RM_XFF);
    const audS = await login(audSeed, AUD_XFF);
    const admin = sessionClient(adminS);
    const rm = sessionClient(rmS);
    const aud = sessionClient(audS);

    // ── LEG 1a — POSITIVE: risk_manager assigns an OPEN case ──────────────────
    const c1 = await seedOpenCase(TENANT_A, "assign-pos");
    const r1a = await rm.post(`/api/fincrime/cases/${c1}/assign`, { assigneeUserId: rmS.userId });
    await sleep();
    const row1a = await caseRow(TENANT_A, c1);
    const ev1a = await eventsFor(TENANT_A, c1);
    const at1a = await attemptCount("FINCRIME_CASE_ASSIGN_ATTEMPT", c1);
    const assigned = ev1a.filter((e) => e.event_type === "CASE_ASSIGNED");
    const s1a = {
      httpStatus: r1a.status,
      durableState: row1a?.state,
      assigneeMatches: row1a?.assigned_investigator_id === rmS.userId,
      dispositionNull: row1a?.disposition === null,
      dispositionReasonNull: row1a?.disposition_reason === null,
      dispositionedAtNull: row1a?.dispositioned_at === null,
      strFiledFalse: row1a?.str_filed === false,
      caseAssignedEvents: assigned.length,
      fromState: assigned[0]?.from_state,
      toState: assigned[0]?.to_state,
      payloadHash64hex: /^[0-9a-f]{64}$/.test(assigned[0]?.payload_hash ?? ""),
      auditFkPresent: typeof assigned[0]?.audit_chain_entry_id === "number",
      attemptAuditsAtLeast1: at1a >= 1,
    };
    const pass1a =
      s1a.httpStatus === 200 &&
      r1a.json?.state === "UNDER_INVESTIGATION" &&
      s1a.durableState === "UNDER_INVESTIGATION" &&
      s1a.assigneeMatches &&
      s1a.dispositionNull &&
      s1a.dispositionReasonNull &&
      s1a.dispositionedAtNull &&
      s1a.strFiledFalse &&
      s1a.caseAssignedEvents === 1 &&
      s1a.fromState === "OPEN" &&
      s1a.toState === "UNDER_INVESTIGATION" &&
      s1a.payloadHash64hex &&
      s1a.auditFkPresent &&
      s1a.attemptAuditsAtLeast1;
    run.leg({
      id: "1a",
      scenario: "POSITIVE risk_manager assigns an OPEN case (OPEN→UNDER_INVESTIGATION)",
      expectation:
        "HTTP 200 state UNDER_INVESTIGATION; durable case advanced + assignee set; disposition cols NULL; exactly one CASE_ASSIGNED event (OPEN→UNDER_INVESTIGATION, 64-hex payload_hash, audit FK); ≥1 ASSIGN_ATTEMPT audit row",
      observed: `HTTP ${r1a.status}; state=${row1a?.state}; assignee=${s1a.assigneeMatches}; disp=${row1a?.disposition}/${row1a?.disposition_reason}/${row1a?.dispositioned_at}; str=${row1a?.str_filed}; CASE_ASSIGNED=${assigned.length}(${assigned[0]?.from_state}->${assigned[0]?.to_state} fk=${assigned[0]?.audit_chain_entry_id}); attemptAudits=${at1a}`,
      verdict: pass1a ? verdict.verified("HTTP 200 + owner read-back of case+event+audit") : verdict.fail(),
      pass: pass1a,
      signals: s1a,
    });

    // ── LEG 1b — DENIAL: auditor cannot mutate (paired with 1a) ───────────────
    const c1b = await seedOpenCase(TENANT_A, "auditor-deny");
    const r1b = await aud.post(`/api/fincrime/cases/${c1b}/assign`, { assigneeUserId: rmS.userId });
    await sleep();
    const row1b = await caseRow(TENANT_A, c1b);
    const ev1b = await eventsFor(TENANT_A, c1b);
    const s1b = { httpStatus: r1b.status, durableState: row1b?.state, events: ev1b.length };
    const pass1b = s1b.httpStatus === 403 && s1b.durableState === "OPEN" && s1b.events === 0;
    run.leg({
      id: "1b",
      scenario: "DENIAL authenticated auditor attempts assign (role not admin/risk_manager)",
      expectation: "HTTP 403; case unchanged (still OPEN); ZERO events (paired positive: 1a risk_manager allowed)",
      observed: `HTTP ${r1b.status}; state=${row1b?.state}; events=${ev1b.length}`,
      verdict: pass1b ? verdict.verified("HTTP 403 + owner read-back proves no mutation") : verdict.fail(),
      pass: pass1b,
      signals: s1b,
    });

    // ── LEG 1c — DENIAL: unauthenticated (valid CSRF, no session) ─────────────
    const c1c = await seedOpenCase(TENANT_A, "unauth-deny");
    const anonJar = new Jar();
    const anonCsrf = await getCsrf(anonJar, ANON_XFF);
    const r1c = await call("POST", `/api/fincrime/cases/${c1c}/assign`, anonJar, anonCsrf, { assigneeUserId: rmS.userId }, ANON_XFF);
    await sleep();
    const row1c = await caseRow(TENANT_A, c1c);
    const s1c = { httpStatus: r1c.status, rejectedUnauth: r1c.status === 401 || r1c.status === 403, durableState: row1c?.state };
    const pass1c = s1c.rejectedUnauth && s1c.durableState === "OPEN";
    run.leg({
      id: "1c",
      scenario: "DENIAL unauthenticated assign (CSRF satisfied, no session)",
      expectation: "HTTP 401/403; case unchanged (paired positive: 1a authenticated)",
      observed: `HTTP ${r1c.status}; state=${row1c?.state}`,
      verdict: pass1c ? verdict.verified("rejected unauthenticated + owner read-back proves no mutation") : verdict.fail(),
      pass: pass1c,
      signals: s1c,
    });

    // ── LEG 1d — POSITIVE: admin starts an investigation WITH a free-text note ─
    const c1d = await seedOpenCase(TENANT_A, "start-pos");
    const r1d = await admin.post(`/api/fincrime/cases/${c1d}/start`, { note: RAW_NOTE });
    await sleep();
    const row1d = await caseRow(TENANT_A, c1d);
    const ev1d = await eventsFor(TENANT_A, c1d);
    const evid1d = await evidenceFor(TENANT_A, c1d);
    const at1d = await attemptCount("FINCRIME_INVESTIGATION_START_ATTEMPT", c1d);
    const started = ev1d.filter((e) => e.event_type === "INVESTIGATION_STARTED");
    const evAdded = ev1d.filter((e) => e.event_type === "EVIDENCE_ADDED");
    const lk1d = await leakProbe(RAW_NOTE, leakSurfaces(c1d));
    const s1d = {
      httpStatus: r1d.status,
      respState: r1d.json?.state,
      evidenceIdString: typeof r1d.json?.evidenceId === "string",
      durableState: row1d?.state,
      startedEvents: started.length,
      fromState: started[0]?.from_state,
      toState: started[0]?.to_state,
      evidenceAddedEvents: evAdded.length,
      evidenceRows: evid1d.length,
      evidenceType: evid1d[0]?.evidence_type,
      encryptedNoteSet: evid1d[0]?.encrypted_note !== null,
      rawNoteAbsentInEvidence: !(evid1d[0]?.encrypted_note ?? "").includes(RAW_NOTE),
      payloadHash64hex: /^[0-9a-f]{64}$/.test(evid1d[0]?.payload_hash ?? ""),
      auditFkPresent: typeof evid1d[0]?.audit_chain_entry_id === "number",
      attemptAuditsAtLeast1: at1d >= 1,
      leakAny: lk1d.any,
    };
    const pass1d =
      s1d.httpStatus === 200 &&
      s1d.respState === "UNDER_INVESTIGATION" &&
      s1d.evidenceIdString &&
      s1d.durableState === "UNDER_INVESTIGATION" &&
      s1d.startedEvents === 1 &&
      s1d.fromState === "OPEN" &&
      s1d.toState === "UNDER_INVESTIGATION" &&
      s1d.evidenceAddedEvents === 1 &&
      s1d.evidenceRows === 1 &&
      s1d.evidenceType === "INVESTIGATION_NOTE" &&
      s1d.encryptedNoteSet &&
      s1d.rawNoteAbsentInEvidence &&
      s1d.payloadHash64hex &&
      s1d.auditFkPresent &&
      s1d.attemptAuditsAtLeast1 &&
      s1d.leakAny === false;
    run.leg({
      id: "1d",
      scenario: "POSITIVE admin starts investigation with a free-text note (transition + encrypted evidence + audit)",
      expectation:
        "HTTP 200 UNDER_INVESTIGATION + evidenceId; one INVESTIGATION_STARTED + one EVIDENCE_ADDED event; one encrypted INVESTIGATION_NOTE evidence row (encrypted_note set, raw note ABSENT); ≥1 START_ATTEMPT audit; raw note appears in NO at-rest surface",
      observed: `HTTP ${r1d.status}; state=${row1d?.state}; STARTED=${started.length}; EVIDENCE_ADDED=${evAdded.length}; evidence=${evid1d.length}(${evid1d[0]?.evidence_type} enc=${s1d.encryptedNoteSet}); attemptAudits=${at1d}; leak{ev=${lk1d.hits["evidence.encrypted_note"]},evt=${lk1d.hits["events.event_payload"]},aud=${lk1d.hits["audit.details"]}}`,
      verdict: pass1d
        ? verdict.verified("HTTP 200 + owner read-back: transition+event+encrypted evidence; raw-note leak probe negative on all 3 surfaces")
        : verdict.fail(),
      pass: pass1d,
      signals: s1d,
    });

    // ── LEG 2a — M3 FENCE (assign): inject state → 400 (red); clean → 200 (green)
    const c2 = await seedOpenCase(TENANT_A, "fence-assign");
    const r2red = await rm.post(`/api/fincrime/cases/${c2}/assign`, { assigneeUserId: rmS.userId, state: "DISPOSITIONED" });
    const rowMid = await caseRow(TENANT_A, c2);
    const r2green = await rm.post(`/api/fincrime/cases/${c2}/assign`, { assigneeUserId: rmS.userId });
    await sleep();
    const row2 = await caseRow(TENANT_A, c2);
    const s2a = {
      redStatus: r2red.status,
      midState: rowMid?.state,
      greenStatus: r2green.status,
      durableState: row2?.state,
      dispositionNull: row2?.disposition === null,
    };
    const pass2a =
      s2a.redStatus === 400 && s2a.midState === "OPEN" && s2a.greenStatus === 200 && s2a.durableState === "UNDER_INVESTIGATION" && s2a.dispositionNull;
    run.leg({
      id: "2a",
      scenario: "M3-FENCE assign — body injects state=DISPOSITIONED (red) then clean assign (green)",
      expectation: "RED: HTTP 400 (strict DTO rejects 'state'), case still OPEN; GREEN: clean assign HTTP 200 → UNDER_INVESTIGATION, disposition still NULL",
      observed: `red HTTP ${r2red.status} (midState=${rowMid?.state}); green HTTP ${r2green.status} (state=${row2?.state}, disp=${row2?.disposition})`,
      verdict: pass2a ? verdict.verified("fenced key 400 leaves OPEN; identical clean body succeeds — rejection IS the fence") : verdict.fail(),
      pass: pass2a,
      signals: s2a,
    });

    // ── LEG 2b — M3 FENCE (start/evidence): every fenced key → 400; clean → ok ─
    const c2b = await seedOpenCase(TENANT_A, "fence-start");
    const startFenced = [
      { note: "x", disposition: "CLEARED" },
      { note: "x", disposition_reason: "because" },
      { note: "x", dispositioned_at: new Date().toISOString() },
      { note: "x", str_filed: true },
      { note: "x", state: "PENDING_MLCO_REVIEW" },
    ];
    const startFencedStatuses: number[] = [];
    for (const b of startFenced) {
      const r = await rm.post(`/api/fincrime/cases/${c2b}/start`, b);
      startFencedStatuses.push(r.status);
    }
    const rowAfterFenced = await caseRow(TENANT_A, c2b);
    const r2bGreen = await rm.post(`/api/fincrime/cases/${c2b}/start`, { note: "clean-start" });
    await sleep();
    const row2b = await caseRow(TENANT_A, c2b);
    const c2c = await seedOpenCase(TENANT_A, "fence-evidence");
    const r2cRed = await rm.post(`/api/fincrime/cases/${c2c}/evidence`, { evidenceType: "DOCUMENT_REF", opaqueRef: `opaque:doc-${base}`, state: "DISPOSITIONED" });
    const r2cGreen = await rm.post(`/api/fincrime/cases/${c2c}/evidence`, { evidenceType: "DOCUMENT_REF", opaqueRef: `opaque:doc-${base}` });
    await sleep();
    const evid2c = await evidenceFor(TENANT_A, c2c);
    const s2b = {
      startFencedAll400: startFencedStatuses.every((s) => s === 400),
      startFencedStatuses,
      midStateOpen: rowAfterFenced?.state === "OPEN",
      cleanStartStatus: r2bGreen.status,
      durableStartState: row2b?.state,
      evidenceRedStatus: r2cRed.status,
      evidenceGreenStatus: r2cGreen.status,
      evidenceRows: evid2c.length,
    };
    const pass2b =
      s2b.startFencedAll400 &&
      s2b.midStateOpen &&
      s2b.cleanStartStatus === 200 &&
      s2b.durableStartState === "UNDER_INVESTIGATION" &&
      s2b.evidenceRedStatus === 400 &&
      s2b.evidenceGreenStatus === 201 &&
      s2b.evidenceRows === 1;
    run.leg({
      id: "2b",
      scenario: "M3-FENCE start/evidence — disposition/state/str_filed keys (red) vs clean body (green)",
      expectation: "every fenced start body 400 (case stays OPEN); clean start 200; evidence fenced 400 then clean 201 with exactly one durable evidence row",
      observed: `startFenced=[${startFencedStatuses.join(",")}] midState=${rowAfterFenced?.state}; cleanStart HTTP ${r2bGreen.status}->${row2b?.state}; evidence red ${r2cRed.status}/green ${r2cGreen.status} rows=${evid2c.length}`,
      verdict: pass2b ? verdict.verified("all fenced keys 400 + clean bodies succeed; owner read-back confirms only clean evidence landed") : verdict.fail(),
      pass: pass2b,
      signals: s2b,
    });

    // ── LEG 2d — M3 FENCE (escalate): inject disposition → 400; case unchanged ─
    const r2dRed = await rm.post(`/api/fincrime/cases/${c2}/escalate-mlco`, { summary: "y", disposition: "STR_RECOMMENDED" });
    await sleep();
    const row2d = await caseRow(TENANT_A, c2);
    const s2d = { redStatus: r2dRed.status, durableState: row2d?.state, dispositionNull: row2d?.disposition === null };
    const pass2d = s2d.redStatus === 400 && s2d.durableState === "UNDER_INVESTIGATION" && s2d.dispositionNull;
    run.leg({
      id: "2d",
      scenario: "M3-FENCE escalate — body injects disposition=STR_RECOMMENDED (red)",
      expectation: "HTTP 400 (strict DTO rejects 'disposition'); case stays UNDER_INVESTIGATION, disposition NULL (paired green escalate is leg 4a)",
      observed: `red HTTP ${r2dRed.status}; state=${row2d?.state}; disp=${row2d?.disposition}`,
      verdict: pass2d ? verdict.verified("escalate cannot smuggle a disposition; owner read-back confirms NULL") : verdict.fail(),
      pass: pass2d,
      signals: s2d,
    });

    // ── LEG 3 — M3 FENCE (post-M3 lockstep refresh; mirrors original battery) ──
    // See original battery leg 3 for full rationale: the "no str route" assertion
    // became a FALSE-RED after M3 two-eyes STR disposal shipped (FU-070/071). Refreshed
    // in lockstep with the original + the equivalence manifest so harness ≡ original;
    // the fence is NOT weakened (architect-ruled, AS/PLATFORM/2026/003).
    const absentDisposingPaths = ["dispose", "close", "disposition", "file-str"];
    const disposeStatuses: Record<string, number> = {};
    for (const p of absentDisposingPaths) {
      const r = await rm.post(`/api/fincrime/cases/${c1}/${p}`, { foo: "bar" });
      disposeStatuses[p] = r.status;
    }
    // (a) DTO fence — malformed STR body rejected at the schema (400, AEGIS_ERR_100).
    const strProbe = await rm.post(`/api/fincrime/cases/${c1}/str`, { foo: "bar" });
    disposeStatuses["str"] = strProbe.status;
    // (b) STATE fence — WELL-FORMED STR body on c1 (UNDER_INVESTIGATION, NOT
    //     PENDING_MLCO_REVIEW) rejected by the disposal STATE precondition
    //     (StrGuardError("STATE_CONFLICT") → 409 AEGIS_ERR_104/state_conflict). Paired
    //     controls: zero fincrime_str_reports rows + narrative in no at-rest surface.
    const strWellFormed = await rm.post(`/api/fincrime/cases/${c1}/str`, { narrative: RAW_SUMMARY });
    const realRoute = await rm.post(`/api/fincrime/cases/${c1}/assign`, { assigneeUserId: rmS.userId });
    const row3 = await caseRow(TENANT_A, c1);
    const strRows3 = await strReportCount(TENANT_A, c1);
    const lk3 = await leakProbe(RAW_SUMMARY, leakSurfaces(c1));
    const strBody = strProbe.json as any;
    const strWfBody = strWellFormed.json as any;
    const s3 = {
      nonStrDisposeStatuses404: absentDisposingPaths.every((p) => disposeStatuses[p] === 404),
      disposeStatuses,
      strStatus: strProbe.status,
      strRejectedByPrepareValidation:
        strProbe.status === 400 &&
        strBody?.error?.code === "AEGIS_ERR_100" &&
        strBody?.error?.details?.validation === "fincrime_str_prepare",
      strWellFormedStatus: strWellFormed.status,
      strWellFormedStateConflict:
        strWellFormed.status === 409 &&
        strWfBody?.error?.code === "AEGIS_ERR_104" &&
        strWfBody?.error?.details?.reason === "state_conflict",
      zeroStrReports: strRows3 === 0,
      rejectedNarrativeNotPersisted: lk3.any === false,
      noDispositionMutation:
        row3?.state !== "DISPOSITIONED" &&
        row3?.disposition === null &&
        row3?.disposition_reason === null &&
        row3?.dispositioned_at === null &&
        row3?.str_filed === false,
      strFiledFalse: row3?.str_filed === false,
      realRouteStatus: realRoute.status,
      realRouteNon404: realRoute.status !== 404,
    };
    const pass3 =
      s3.nonStrDisposeStatuses404 &&
      s3.strRejectedByPrepareValidation &&
      s3.strWellFormedStateConflict &&
      s3.zeroStrReports &&
      s3.rejectedNarrativeNotPersisted &&
      s3.noDispositionMutation &&
      s3.realRouteNon404;
    run.leg({
      id: "3",
      scenario: "M3-FENCE (post-M3) — non-STR disposal verbs absent; shipped STR route DTO-fences a malformed body AND STATE-fences a well-formed M2-context disposal; zero STR rows; no disposition effect",
      expectation: "dispose/close/disposition/file-str → 404 (absent); str{malformed} → 400 (AEGIS_ERR_100/fincrime_str_prepare); str{well-formed narrative} → 409 (AEGIS_ERR_104/state_conflict; c1 UNDER_INVESTIGATION≠PENDING_MLCO_REVIEW); zero fincrime_str_reports rows; rejected narrative in no at-rest surface; case not DISPOSITIONED, disposition cols NULL, str_filed false; assign reachable (409)",
      observed: `dispose=${JSON.stringify(disposeStatuses)}; strWellFormed HTTP ${strWellFormed.status}(${strWfBody?.error?.code}/${strWfBody?.error?.details?.reason}); strRows=${strRows3}; narrativePersisted=${lk3.any}; realAssign HTTP ${realRoute.status}; state=${row3?.state}; disp=${row3?.disposition}; strFiled=${row3?.str_filed}`,
      verdict: pass3
        ? verdict.verified("non-STR verbs 404; STR DTO-fenced 400 AND STATE-fenced 409/state_conflict; owner read-back: zero STR rows, narrative not persisted, no disposition; real route 409")
        : verdict.fail(),
      pass: pass3,
      signals: s3,
    });

    // ── LEG 4a — POSITIVE: escalate UNDER_INVESTIGATION → PENDING_MLCO_REVIEW ──
    const c4 = await seedOpenCase(TENANT_A, "escalate");
    await rm.post(`/api/fincrime/cases/${c4}/assign`, { assigneeUserId: rmS.userId });
    const r4a = await rm.post(`/api/fincrime/cases/${c4}/escalate-mlco`, { summary: RAW_SUMMARY });
    await sleep();
    const row4a = await caseRow(TENANT_A, c4);
    const ev4a = await eventsFor(TENANT_A, c4);
    const evid4a = await evidenceFor(TENANT_A, c4);
    const at4a = await attemptCount("FINCRIME_ESCALATE_MLCO_ATTEMPT", c4);
    const escEvents = ev4a.filter((e) => e.event_type === "ESCALATED_TO_MLCO_REVIEW");
    const noteEvid = evid4a.filter((e) => e.evidence_type === "INVESTIGATION_NOTE");
    const lk4a = await leakProbe(RAW_SUMMARY, leakSurfaces(c4));
    const s4a = {
      httpStatus: r4a.status,
      respState: r4a.json?.state,
      evidenceIdString: typeof r4a.json?.evidenceId === "string",
      durableState: row4a?.state,
      dispositionNull: row4a?.disposition === null,
      escalatedEvents: escEvents.length,
      fromState: escEvents[0]?.from_state,
      toState: escEvents[0]?.to_state,
      noteEvidence: noteEvid.length,
      encryptedNoteSet: noteEvid[0]?.encrypted_note !== null,
      rawSummaryAbsent: !(noteEvid[0]?.encrypted_note ?? "").includes(RAW_SUMMARY),
      attemptAuditsAtLeast1: at4a >= 1,
      leakAny: lk4a.any,
    };
    const pass4a =
      s4a.httpStatus === 200 &&
      s4a.respState === "PENDING_MLCO_REVIEW" &&
      s4a.evidenceIdString &&
      s4a.durableState === "PENDING_MLCO_REVIEW" &&
      s4a.dispositionNull &&
      s4a.escalatedEvents === 1 &&
      s4a.fromState === "UNDER_INVESTIGATION" &&
      s4a.toState === "PENDING_MLCO_REVIEW" &&
      s4a.noteEvidence === 1 &&
      s4a.encryptedNoteSet &&
      s4a.rawSummaryAbsent &&
      s4a.attemptAuditsAtLeast1 &&
      s4a.leakAny === false;
    run.leg({
      id: "4a",
      scenario: "POSITIVE escalate-mlco (UNDER_INVESTIGATION→PENDING_MLCO_REVIEW) with required encrypted summary",
      expectation:
        "HTTP 200 PENDING_MLCO_REVIEW + evidenceId; disposition NULL (terminal, NOT disposed); one ESCALATED_TO_MLCO_REVIEW event; one encrypted INVESTIGATION_NOTE summary (raw ABSENT); ≥1 ESCALATE_ATTEMPT audit; raw summary in NO at-rest surface",
      observed: `HTTP ${r4a.status}; state=${row4a?.state}; disp=${row4a?.disposition}; ESCALATED=${escEvents.length}(${escEvents[0]?.from_state}->${escEvents[0]?.to_state}); noteEvidence=${noteEvid.length}(enc=${s4a.encryptedNoteSet}); attemptAudits=${at4a}; leak{ev=${lk4a.hits["evidence.encrypted_note"]},evt=${lk4a.hits["events.event_payload"]},aud=${lk4a.hits["audit.details"]}}`,
      verdict: pass4a
        ? verdict.verified("HTTP 200 + owner read-back: terminal transition, NULL disposition, encrypted summary, leak probe negative")
        : verdict.fail(),
      pass: pass4a,
      signals: s4a,
    });

    // ── LEG 4b — M3 FENCE: PENDING_MLCO_REVIEW is terminal for M2 ──────────────
    const t1 = await rm.post(`/api/fincrime/cases/${c4}/assign`, { assigneeUserId: rmS.userId });
    const t2 = await rm.post(`/api/fincrime/cases/${c4}/start`, { note: "x" });
    const t3 = await rm.post(`/api/fincrime/cases/${c4}/escalate-mlco`, { summary: "again" });
    const t4 = await rm.post(`/api/fincrime/cases/${c4}/evidence`, { evidenceType: "KYT_REF", opaqueRef: `opaque:k-${base}` });
    await sleep();
    const row4b = await caseRow(TENANT_A, c4);
    const evid4b = await evidenceFor(TENANT_A, c4);
    const s4b = {
      assignStatus: t1.status,
      startStatus: t2.status,
      escalateStatus: t3.status,
      evidenceStatus: t4.status,
      durableState: row4b?.state,
      dispositionNull: row4b?.disposition === null,
      evidenceUnchanged: evid4b.length === noteEvid.length,
    };
    const pass4b =
      s4b.assignStatus === 409 &&
      s4b.startStatus === 409 &&
      s4b.escalateStatus === 409 &&
      s4b.evidenceStatus === 409 &&
      s4b.durableState === "PENDING_MLCO_REVIEW" &&
      s4b.dispositionNull &&
      s4b.evidenceUnchanged;
    run.leg({
      id: "4b",
      scenario: "M3-FENCE terminal — PENDING_MLCO_REVIEW case cannot be advanced/dispositioned by ANY M2 action",
      expectation: "assign/start/escalate all 409 (CAS wrong from-state); evidence 409 (state not eligible); case stays PENDING_MLCO_REVIEW, disposition NULL, no new evidence",
      observed: `assign=${t1.status} start=${t2.status} escalate=${t3.status} evidence=${t4.status}; state=${row4b?.state}; disp=${row4b?.disposition}; evidenceUnchanged=${s4b.evidenceUnchanged}`,
      verdict: pass4b
        ? verdict.verified("every onward M2 action 409 + owner read-back: state/disposition unchanged — M2 cannot reach disposition")
        : verdict.fail(),
      pass: pass4b,
      signals: s4b,
    });

    // ── LEG 5a — POSITIVE: add opaque-only evidence to an OPEN case ────────────
    const c5 = await seedOpenCase(TENANT_A, "evidence-pos");
    const r5a = await rm.post(`/api/fincrime/cases/${c5}/evidence`, { evidenceType: "TRANSACTION_REF", opaqueRef: `opaque:txn-${base}` });
    await sleep();
    const row5a = await caseRow(TENANT_A, c5);
    const evid5a = await evidenceFor(TENANT_A, c5);
    const evAdded5a = (await eventsFor(TENANT_A, c5)).filter((e) => e.event_type === "EVIDENCE_ADDED");
    const at5a = await attemptCount("FINCRIME_EVIDENCE_ADD_ATTEMPT", c5);
    const s5a = {
      httpStatus: r5a.status,
      evidenceIdString: typeof r5a.json?.evidenceId === "string",
      durableState: row5a?.state,
      evidenceRows: evid5a.length,
      evidenceType: evid5a[0]?.evidence_type,
      opaqueRefMatches: evid5a[0]?.opaque_ref === `opaque:txn-${base}`,
      encryptedNoteNull: evid5a[0]?.encrypted_note === null,
      auditFkPresent: typeof evid5a[0]?.audit_chain_entry_id === "number",
      evidenceAddedEvents: evAdded5a.length,
      attemptAuditsAtLeast1: at5a >= 1,
    };
    const pass5a =
      s5a.httpStatus === 201 &&
      s5a.evidenceIdString &&
      s5a.durableState === "OPEN" &&
      s5a.evidenceRows === 1 &&
      s5a.evidenceType === "TRANSACTION_REF" &&
      s5a.opaqueRefMatches &&
      s5a.encryptedNoteNull &&
      s5a.auditFkPresent &&
      s5a.evidenceAddedEvents === 1 &&
      s5a.attemptAuditsAtLeast1;
    run.leg({
      id: "5a",
      scenario: "POSITIVE add opaque-only evidence (no state change)",
      expectation: "HTTP 201 + evidenceId; case stays OPEN; one TRANSACTION_REF evidence row (opaque_ref set, encrypted_note NULL, audit FK); one EVIDENCE_ADDED event; ≥1 ADD_ATTEMPT audit",
      observed: `HTTP ${r5a.status}; state=${row5a?.state}; evidence=${evid5a.length}(${evid5a[0]?.evidence_type} opaque=${s5a.opaqueRefMatches}); EVIDENCE_ADDED=${evAdded5a.length}; attemptAudits=${at5a}`,
      verdict: pass5a ? verdict.verified("HTTP 201 + owner read-back of evidence row + event + audit; case state unchanged") : verdict.fail(),
      pass: pass5a,
      signals: s5a,
    });

    // ── LEG 5b — DENIAL: malformed evidence (paired with 5a) ──────────────────
    const r5bNoContent = await rm.post(`/api/fincrime/cases/${c5}/evidence`, { evidenceType: "TRANSACTION_REF" });
    const r5bBadType = await rm.post(`/api/fincrime/cases/${c5}/evidence`, { evidenceType: "NOT_A_TYPE", opaqueRef: `opaque:x-${base}` });
    const r5bPiiRef = await rm.post(`/api/fincrime/cases/${c5}/evidence`, { evidenceType: "DOCUMENT_REF", opaqueRef: "john.doe@example.com" });
    await sleep();
    const evid5b = await evidenceFor(TENANT_A, c5);
    const s5b = {
      noContentStatus: r5bNoContent.status,
      badTypeStatus: r5bBadType.status,
      piiRefStatus: r5bPiiRef.status,
      evidenceRows: evid5b.length,
    };
    const pass5b = s5b.noContentStatus === 400 && s5b.badTypeStatus === 400 && s5b.piiRefStatus === 400 && s5b.evidenceRows === 1;
    run.leg({
      id: "5b",
      scenario: "DENIAL malformed evidence — no content / bad type / raw-PII opaqueRef",
      expectation: "all three HTTP 400; evidence row count unchanged at 1 (paired positive: 5a opaque evidence accepted)",
      observed: `noContent=${r5bNoContent.status} badType=${r5bBadType.status} piiRef=${r5bPiiRef.status}; evidenceRows=${evid5b.length}`,
      verdict: pass5b ? verdict.verified("all malformed 400 + owner read-back: no malformed evidence landed") : verdict.fail(),
      pass: pass5b,
      signals: s5b,
    });

    // ── LEG 6a — tenant-from-session: A-actor cannot assign a B case ──────────
    const cB = await seedOpenCase(TENANT_B, "xtenant");
    const cAok = await seedOpenCase(TENANT_A, "xtenant-control");
    const r6aDeny = await rm.post(`/api/fincrime/cases/${cB}/assign`, { assigneeUserId: rmS.userId });
    const r6aOk = await rm.post(`/api/fincrime/cases/${cAok}/assign`, { assigneeUserId: rmS.userId });
    await sleep();
    const rowB = await caseRow(TENANT_B, cB);
    const evB = await eventsFor(TENANT_B, cB);
    const rowAok = await caseRow(TENANT_A, cAok);
    const seenA = await rlsVisible(TENANT_A, cB);
    const seenB = await rlsVisible(TENANT_B, cB);
    const s6a = {
      denyStatus: r6aDeny.status,
      bDurableState: rowB?.state,
      bEvents: evB.length,
      controlStatus: r6aOk.status,
      controlDurableState: rowAok?.state,
      rlsVisibleA: seenA,
      rlsVisibleB: seenB,
    };
    const pass6a =
      s6a.denyStatus === 409 &&
      s6a.bDurableState === "OPEN" &&
      s6a.bEvents === 0 &&
      s6a.controlStatus === 200 &&
      s6a.controlDurableState === "UNDER_INVESTIGATION" &&
      s6a.rlsVisibleA === 0 &&
      s6a.rlsVisibleB === 1;
    run.leg({
      id: "6a",
      scenario: "DENIAL tenant-from-session — A-authed actor assigns a TENANT_B case id",
      expectation: "HTTP 409 (no matching case in the actor's session tenant); B case untouched (OPEN, 0 events); paired A-case assign 200; RLS: B case invisible under withTenantRls(A)=0, visible under (B)=1",
      observed: `denyB HTTP ${r6aDeny.status} (Bstate=${rowB?.state}, Bevents=${evB.length}); controlA HTTP ${r6aOk.status} (state=${rowAok?.state}); rlsVisible A=${seenA} B=${seenB}`,
      verdict: pass6a
        ? verdict.verified("tenant derives from session not path; owner+RLS read-back prove the B case is untouched and RLS-isolated")
        : verdict.fail(),
      pass: pass6a,
      signals: s6a,
    });

    // ── LEG 6b — cross-tenant evidence add → 404 (paired with 5a positive) ────
    const r6b = await rm.post(`/api/fincrime/cases/${cB}/evidence`, { evidenceType: "KYT_REF", opaqueRef: `opaque:xt-${base}` });
    await sleep();
    const evidB = await evidenceFor(TENANT_B, cB);
    const s6b = { httpStatus: r6b.status, bEvidenceRows: evidB.length };
    const pass6b = s6b.httpStatus === 404 && s6b.bEvidenceRows === 0;
    run.leg({
      id: "6b",
      scenario: "DENIAL cross-tenant evidence — A-authed actor adds evidence to a TENANT_B case",
      expectation: "HTTP 404 (case not found in the actor's tenant); ZERO evidence rows on the B case (paired positive: 5a)",
      observed: `HTTP ${r6b.status}; B evidence rows=${evidB.length}`,
      verdict: pass6b ? verdict.verified("404 + owner read-back proves no evidence landed cross-tenant") : verdict.fail(),
      pass: pass6b,
      signals: s6b,
    });

    // ── LEG 6c — payload cannot assert tenant (strict DTO) ────────────────────
    const c6c = await seedOpenCase(TENANT_A, "tenant-assert");
    const r6cCamel = await rm.post(`/api/fincrime/cases/${c6c}/assign`, { assigneeUserId: rmS.userId, tenantId: TENANT_B });
    const r6cSnake = await rm.post(`/api/fincrime/cases/${c6c}/assign`, { assigneeUserId: rmS.userId, tenant_id: TENANT_B });
    await sleep();
    const row6c = await caseRow(TENANT_A, c6c);
    const s6c = { camelStatus: r6cCamel.status, snakeStatus: r6cSnake.status, durableState: row6c?.state };
    const pass6c = s6c.camelStatus === 400 && s6c.snakeStatus === 400 && s6c.durableState === "OPEN";
    run.leg({
      id: "6c",
      scenario: "DENIAL payload-asserted tenant — body carries tenantId/tenant_id = B",
      expectation: "HTTP 400 on both (strict DTO rejects the unknown tenant key); case unchanged (OPEN) — tenant is never client-supplied",
      observed: `tenantId=${r6cCamel.status} tenant_id=${r6cSnake.status}; state=${row6c?.state}`,
      verdict: pass6c ? verdict.verified("payload cannot assert tenant; owner read-back confirms no mutation") : verdict.fail(),
      pass: pass6c,
      signals: s6c,
    });

    // ── LEG 7 — assignee no-oracle validation (identical 400 for all bad cases) ─
    const c7valid = await seedOpenCase(TENANT_A, "assignee-valid");
    const r7valid = await rm.post(`/api/fincrime/cases/${c7valid}/assign`, { assigneeUserId: rm2Seed.id });
    const c7a = await seedOpenCase(TENANT_A, "assignee-missing");
    const r7missing = await rm.post(`/api/fincrime/cases/${c7a}/assign`, { assigneeUserId: randomUUID() });
    const c7b = await seedOpenCase(TENANT_A, "assignee-wrongrole");
    const r7wrongRole = await rm.post(`/api/fincrime/cases/${c7b}/assign`, { assigneeUserId: audSeed.id });
    const c7c = await seedOpenCase(TENANT_A, "assignee-xtenant");
    const r7xtenant = await rm.post(`/api/fincrime/cases/${c7c}/assign`, { assigneeUserId: bUserSeed.id });
    await sleep();
    const row7valid = await caseRow(TENANT_A, c7valid);
    const badStatuses = [r7missing.status, r7wrongRole.status, r7xtenant.status];
    const badBodies = [
      (r7missing.json?.error as { code?: string } | undefined)?.code,
      (r7wrongRole.json?.error as { code?: string } | undefined)?.code,
      (r7xtenant.json?.error as { code?: string } | undefined)?.code,
    ];
    const s7 = {
      validStatus: r7valid.status,
      validDurableState: row7valid?.state,
      validAssigneeMatches: row7valid?.assigned_investigator_id === rm2Seed.id,
      badStatuses,
      badAll400: badStatuses.every((s) => s === 400),
      errorShapeIdentical: new Set(badBodies).size === 1,
    };
    const pass7 = s7.validStatus === 200 && s7.validDurableState === "UNDER_INVESTIGATION" && s7.validAssigneeMatches && s7.badAll400 && s7.errorShapeIdentical;
    run.leg({
      id: "7",
      scenario: "ASSIGNEE no-oracle — valid assignee accepted; non-existent / wrong-role / cross-tenant ALL identical 400",
      expectation: "valid eligible assignee HTTP 200 (case assigned to them); the three ineligible cases ALL return HTTP 400 with an IDENTICAL error shape (no existence/role oracle)",
      observed: `valid HTTP ${r7valid.status} (assignee=${s7.validAssigneeMatches}); bad=[${badStatuses.join(",")}]; codes=[${badBodies.join(",")}] identical=${s7.errorShapeIdentical}`,
      verdict: pass7
        ? verdict.verified("HTTP 200 valid + owner read-back; three distinct ineligibility reasons collapse to one indistinguishable 400")
        : verdict.fail(),
      pass: pass7,
      signals: s7,
    });

    // ── LEG 8 — CAS concurrency: two concurrent assigns ⇒ exactly one 409 ──────
    const c8 = await seedOpenCase(TENANT_A, "concurrency");
    const [c8r1, c8r2] = await Promise.all([
      rm.post(`/api/fincrime/cases/${c8}/assign`, { assigneeUserId: rmS.userId }),
      rm.post(`/api/fincrime/cases/${c8}/assign`, { assigneeUserId: rm2Seed.id }),
    ]);
    await sleep();
    const row8 = await caseRow(TENANT_A, c8);
    const ev8 = (await eventsFor(TENANT_A, c8)).filter((e) => e.event_type === "CASE_ASSIGNED");
    const at8 = await attemptCount("FINCRIME_CASE_ASSIGN_ATTEMPT", c8);
    const statuses = [c8r1.status, c8r2.status].sort();
    const s8 = {
      statusesSorted: statuses,
      oneWonOne409: statuses[0] === 200 && statuses[1] === 409,
      durableState: row8?.state,
      caseAssignedEvents: ev8.length,
      attemptAudits: at8,
    };
    const pass8 = s8.oneWonOne409 && s8.durableState === "UNDER_INVESTIGATION" && s8.caseAssignedEvents === 1;
    run.leg({
      id: "8",
      scenario: "CAS concurrency — two concurrent assigns on one OPEN case",
      expectation: "exactly one HTTP 200 and one HTTP 409; durable case UNDER_INVESTIGATION with EXACTLY ONE CASE_ASSIGNED event (effect-once via the compare-and-set)",
      observed: `statuses=[${statuses.join(",")}]; state=${row8?.state}; CASE_ASSIGNED events=${ev8.length}; attemptAudits=${at8}`,
      verdict: pass8
        ? `PASS — functionally-verified (HTTP {one 200, one 409} + owner read-back: exactly one transition event). HONEST CAVEAT (Rule 19): attempt-before-effect means ≥1 ASSIGN_ATTEMPT audit rows (observed ${at8}); the EFFECT, not the attempt, is exactly-once.`
        : verdict.fail(),
      pass: pass8,
      signals: s8,
    });

    // ── LEG 9a — read route: auditor CAN read; projection omits disposition ────
    const r9a = await aud.get(`/api/fincrime/cases?queue=all-active`);
    const ourIds = [...createdCaseIds];
    const r9aCases = (r9a.json?.cases as Array<Record<string, unknown>> | undefined) ?? [];
    const returnedOurs = r9aCases.filter((c) => ourIds.includes(c.id as string));
    const noDispositionLeak = r9aCases.every((c) => !("disposition" in c) && !("dispositionReason" in c) && !("strFiled" in c));
    const s9a = {
      httpStatus: r9a.status,
      casesIsArray: Array.isArray(r9a.json?.cases),
      returnedOursGt0: returnedOurs.length > 0,
      dispositionOmitted: noDispositionLeak,
    };
    const pass9a = s9a.httpStatus === 200 && s9a.casesIsArray && s9a.returnedOursGt0 && s9a.dispositionOmitted;
    run.leg({
      id: "9a",
      scenario: "READ route — auditor reads the case queue; projection omits disposition columns",
      expectation: "HTTP 200; auditor (read-only role) IS authorized to list; payload carries NO disposition/dispositionReason/strFiled keys (paired denial: 1b auditor cannot mutate)",
      observed: `HTTP ${r9a.status}; cases=${r9aCases.length}; ofOurs=${returnedOurs.length}; dispositionOmitted=${noDispositionLeak}`,
      verdict: pass9a ? verdict.verified("HTTP 200 list for auditor; explicit projection check confirms disposition columns are not exposed") : verdict.fail(),
      pass: pass9a,
      signals: s9a,
    });

    // ── LEG 9b — read route: bad queue → 400; valid queue → 200 ───────────────
    const r9bBad = await rm.get(`/api/fincrime/cases?queue=bogus-queue`);
    const r9bGood = await rm.get(`/api/fincrime/cases?queue=open`);
    const s9b = { badStatus: r9bBad.status, goodStatus: r9bGood.status };
    const pass9b = s9b.badStatus === 400 && s9b.goodStatus === 200;
    run.leg({
      id: "9b",
      scenario: "READ route — unknown queue rejected; valid queue accepted",
      expectation: "queue=bogus-queue HTTP 400 (closed enum); queue=open HTTP 200 (paired control)",
      observed: `bad HTTP ${r9bBad.status}; good HTTP ${r9bGood.status}`,
      verdict: pass9b ? verdict.verified("closed-enum queue selector rejects unknown, accepts valid") : verdict.fail(),
      pass: pass9b,
      signals: s9b,
    });

    // ── LEG 9c — read route: queue filters segregate states correctly ─────────
    const ourSet = new Set(createdCaseIds);
    const openQ = await rm.get(`/api/fincrime/cases?queue=open`);
    const pendingQ = await rm.get(`/api/fincrime/cases?queue=pending-mlco-review`);
    const mineQ = await rm.get(`/api/fincrime/cases?queue=mine`);
    const openCases = (openQ.json?.cases as Array<Record<string, unknown>> | undefined) ?? [];
    const pendingCases = (pendingQ.json?.cases as Array<Record<string, unknown>> | undefined) ?? [];
    const mineCases = (mineQ.json?.cases as Array<Record<string, unknown>> | undefined) ?? [];
    const openOurs = openCases.filter((c) => ourSet.has(c.id as string));
    const pendingOurs = pendingCases.filter((c) => ourSet.has(c.id as string));
    const mineOurs = mineCases.filter((c) => ourSet.has(c.id as string));
    const openAllOpen = openOurs.every((c) => c.state === "OPEN");
    const pendingHasC4 = pendingOurs.some((c) => c.id === c4) && pendingOurs.every((c) => c.state === "PENDING_MLCO_REVIEW");
    const mineAllRm = mineOurs.every((c) => c.assignedInvestigatorId === rmS.userId) && mineOurs.some((c) => c.id === c1);
    const s9c = {
      openStatus: openQ.status,
      pendingStatus: pendingQ.status,
      mineStatus: mineQ.status,
      openAllOpen,
      pendingHasC4AndAllPending: pendingHasC4,
      mineAllRmAndHasC1: mineAllRm,
    };
    const pass9c = openQ.status === 200 && pendingQ.status === 200 && mineQ.status === 200 && openAllOpen && pendingHasC4 && mineAllRm;
    run.leg({
      id: "9c",
      scenario: "READ route — open / pending-mlco-review / mine queues segregate by state and assignee",
      expectation: "open returns only OPEN; pending-mlco-review returns only PENDING_MLCO_REVIEW (incl. escalated c4); mine returns only cases assigned to the actor (incl. c1)",
      observed: `open ours=${openOurs.length} allOpen=${openAllOpen}; pending ours=${pendingOurs.length} hasC4&allPending=${pendingHasC4}; mine ours=${mineOurs.length} allRm&hasC1=${mineAllRm}`,
      verdict: pass9c ? verdict.verified("each queue's durable rows match its state/assignee predicate") : verdict.fail(),
      pass: pass9c,
      signals: s9c,
    });

    // ── LEG 11a — DETERMINISTIC TOCTOU proof (denial): held-lock primitive ─────
    const c11a = await seedOpenCase(TENANT_A, "race-deny");
    await rm.post(`/api/fincrime/cases/${c11a}/assign`, { assigneeUserId: rmS.userId });
    const proofA = await heldLockProof({
      tenantId: TENANT_A,
      lockSql: "SELECT id FROM fincrime_cases WHERE tenant_id=$1 AND id=$2 FOR UPDATE",
      lockParams: [TENANT_A, c11a],
      contend: () => rm.post(`/api/fincrime/cases/${c11a}/evidence`, { evidenceType: "DOCUMENT_REF", opaqueRef: `opaque:doc-raceA-${base}` }),
      mutateDuringHold: {
        sql: "UPDATE fincrime_cases SET state='PENDING_MLCO_REVIEW', updated_at=NOW() WHERE tenant_id=$1 AND id=$2",
        params: [TENANT_A, c11a],
      },
    });
    await sleep();
    const row11a = await caseRow(TENANT_A, c11a);
    const docRefA = (await evidenceFor(TENANT_A, c11a)).filter((e) => e.evidence_type === "DOCUMENT_REF");
    const s11a = {
      validTest: proofA.validTest,
      blockedOnRowLock: proofA.blocked,
      evidenceStatus: proofA.result.status,
      docRefRows: docRefA.length,
      durableState: row11a?.state,
    };
    const pass11a = s11a.validTest && s11a.blockedOnRowLock && s11a.evidenceStatus === 409 && s11a.docRefRows === 0 && s11a.durableState === "PENDING_MLCO_REVIEW";
    run.leg({
      id: "11a",
      scenario: "TOCTOU race (denial) — evidence-add concurrent with escalation to PENDING_MLCO_REVIEW",
      expectation: "test-valid (no killing timeouts); evidence-add BLOCKS on the case row lock during the hold; after the case is escalated and the lock releases, the add re-reads the terminal state and returns 409; ZERO DOCUMENT_REF evidence rows; case PENDING_MLCO_REVIEW",
      observed: `validTest=${proofA.validTest}${proofA.validityReasons.length ? ` reasons=${JSON.stringify(proofA.validityReasons)}` : ""} (owner=${JSON.stringify(proofA.ownerTimeouts)} app=${JSON.stringify(proofA.appTimeouts)}); blockedOnRowLock=${proofA.blocked}; evidence HTTP ${proofA.result.status}; docRefRows=${docRefA.length}; state=${row11a?.state}`,
      verdict: pass11a
        ? verdict.structuralDevProven("FOR UPDATE forced serialization: validity pre-checked, evidence blocked behind the row lock, then 409 on the now-terminal case; owner read-back confirms no evidence landed. Bug-distinguishing: a plain SELECT would not block and would 201 mid-hold")
        : verdict.fail(),
      pass: pass11a,
      signals: s11a,
    });

    // ── LEG 11b — DETERMINISTIC TOCTOU proof (positive, paired with 11a) ───────
    const c11b = await seedOpenCase(TENANT_A, "race-allow");
    await rm.post(`/api/fincrime/cases/${c11b}/assign`, { assigneeUserId: rmS.userId });
    const proofB = await heldLockProof({
      tenantId: TENANT_A,
      lockSql: "SELECT id FROM fincrime_cases WHERE tenant_id=$1 AND id=$2 FOR UPDATE",
      lockParams: [TENANT_A, c11b],
      contend: () => rm.post(`/api/fincrime/cases/${c11b}/evidence`, { evidenceType: "DOCUMENT_REF", opaqueRef: `opaque:doc-raceB-${base}` }),
      // no mutateDuringHold — case stays UNDER_INVESTIGATION through the release
    });
    await sleep();
    const row11b = await caseRow(TENANT_A, c11b);
    const docRefB = (await evidenceFor(TENANT_A, c11b)).filter((e) => e.evidence_type === "DOCUMENT_REF");
    const s11b = {
      validTest: proofB.validTest,
      blockedOnRowLock: proofB.blocked,
      evidenceStatus: proofB.result.status,
      docRefRows: docRefB.length,
      durableState: row11b?.state,
    };
    const pass11b = s11b.validTest && s11b.blockedOnRowLock && s11b.evidenceStatus === 201 && s11b.docRefRows === 1 && s11b.durableState === "UNDER_INVESTIGATION";
    run.leg({
      id: "11b",
      scenario: "TOCTOU race (positive, paired) — evidence-add blocks then SUCCEEDS when the case is NOT escalated during the wait",
      expectation: "test-valid; evidence-add BLOCKS on the row lock; after release with the case still UNDER_INVESTIGATION, the add returns 201 and lands exactly one DOCUMENT_REF evidence row; case stays UNDER_INVESTIGATION",
      observed: `validTest=${proofB.validTest}${proofB.validityReasons.length ? ` reasons=${JSON.stringify(proofB.validityReasons)}` : ""}; blockedOnRowLock=${proofB.blocked}; evidence HTTP ${proofB.result.status}; docRefRows=${docRefB.length}; state=${row11b?.state}`,
      verdict: pass11b
        ? verdict.structuralDevProven("same lock-wait as 11a; outcome flips to 201 solely because no escalation occurred — proves 11a's 409 is the state gate, not a broken endpoint")
        : verdict.fail(),
      pass: pass11b,
      signals: s11b,
    });

    // ── LEG 12 — REAL-ROUTE concurrency: evidence-add vs escalate-mlco ─────────
    const N = 8;
    const raceOutcomes: string[] = [];
    let leg12ok = true;
    for (let i = 0; i < N; i++) {
      const cR = await seedOpenCase(TENANT_A, `race12-${i}`);
      await rm.post(`/api/fincrime/cases/${cR}/assign`, { assigneeUserId: rmS.userId });
      const [er, sr] = await Promise.all([
        rm.post(`/api/fincrime/cases/${cR}/evidence`, { evidenceType: "DOCUMENT_REF", opaqueRef: `opaque:doc12-${base}-${i}` }),
        rm.post(`/api/fincrime/cases/${cR}/escalate-mlco`, { summary: `race summary ${i}` }),
      ]);
      await sleep(120);
      const rowR = await caseRow(TENANT_A, cR);
      const docRef = (await evidenceFor(TENANT_A, cR)).filter((e) => e.evidence_type === "DOCUMENT_REF");
      const legalA = er.status === 201 && docRef.length === 1;
      const legalB = er.status === 409 && docRef.length === 0;
      const ok = sr.status === 200 && rowR?.state === "PENDING_MLCO_REVIEW" && (legalA || legalB);
      if (!ok) leg12ok = false;
      raceOutcomes.push(`ev${er.status}/esc${sr.status}/${rowR?.state}/doc${docRef.length}`);
    }
    const countA = raceOutcomes.filter((o) => o.startsWith("ev201")).length;
    const countB = raceOutcomes.filter((o) => o.startsWith("ev409")).length;
    const s12 = {
      iterations: N,
      allLegalShapes: leg12ok,
      serializedBefore201: countA,
      lostToEscalate409: countB,
    };
    run.leg({
      id: "12",
      scenario: `REAL-ROUTE race — evidence-add vs escalate-mlco, ${N} concurrent iterations`,
      expectation: "every iteration is a LEGAL shape: escalate 200, case PENDING_MLCO_REVIEW, and (evidence 201 ⇔ exactly 1 DOCUMENT_REF row) XOR (evidence 409 ⇔ 0 rows); NO evidence row on a case where it landed after PENDING",
      observed: `outcomes=[${raceOutcomes.join(", ")}]; serialized-before(201)=${countA}, lost-to-escalate(409)=${countB}`,
      verdict: leg12ok
        ? "PASS — functionally-verified (all iterations legal; owner read-back confirms evidence-row presence matches the HTTP result and the case is always terminal). Rule-19 note: scheduling decides which legal ordering occurs each run; both are correct and the deterministic gate proof is 11a/11b."
        : verdict.fail(),
      pass: leg12ok,
      signals: s12,
    });

    // ── LEG 10 (comment "LEG 13") — no-silent-close global ────────────────────
    const dispCount = await dispositionedAmong([...createdCaseIds]);
    const withDispCount = await withDispositionAmong([...createdCaseIds]);
    const s10 = { dispositionedCases: dispCount, casesWithAnyDispositionField: withDispCount, totalTouched: createdCaseIds.size };
    const pass10 = dispCount === 0 && withDispCount === 0;
    run.leg({
      id: "10",
      scenario: "M3-FENCE global — no synthetic case reached DISPOSITIONED or carries any disposition field",
      expectation: "across ALL cases this battery touched: ZERO in state DISPOSITIONED; ZERO carrying disposition / disposition_reason / dispositioned_at / str_filed",
      observed: `dispositionedCases=${dispCount}; casesWithAnyDispositionField=${withDispCount}; ofTotal=${createdCaseIds.size}`,
      verdict: pass10
        ? verdict.verified("owner read-back over every touched case: M2 left the disposition surface entirely NULL — no silent close, no M3 reach")
        : verdict.fail(),
      pass: pass10,
      signals: s10,
    });
  } finally {
    try {
      const ids = [...createdCaseIds];
      if (ids.length) {
        const evd = await deleteByColIn("fincrime_case_evidence", "case_id", ids);
        const evt = await deleteByColIn("fincrime_case_events", "case_id", ids);
        const cse = await deleteByColIn("fincrime_cases", "id", ids);
        console.log(`\ncleanup: deleted ${evd} evidence, ${evt} event, ${cse} case row(s).`);
      }
    } catch (e) {
      console.log(`cleanup cases/events/evidence (non-fatal): ${(e as Error)?.message ?? e}`);
    }
    await deactivateUsers(allSeeds.map((s) => s.id));
    console.log("cleanup: synthetic seed users deactivated + credentials neutralized (audit chain preserved).");
    void sql;
    void withTenantRls;
    await run.writeJson();
    await endOwnerPool();
  }

  run.report();
  process.exit(run.acceptance().allPass ? 0 : 1);
}

main().catch((e) => {
  console.error("FATAL harness error:", e);
  process.exit(1);
});
