// ============================================================================
// VF-RP05 — Threat-Intelligence read-path cross-tenant isolation proof (F-RP-05)
// ----------------------------------------------------------------------------
// ref AS/CYBER/READPATH-FIX/THREATINTEL | env: DEV | HOLD at publish gate
//
// WHAT THIS PROVES
//   F-RP-05 (MEDIUM): the threat-intel read surfaces (getFeeds / getIndicators /
//   getRecentMatches / getStats / checkValue / bulkCheck) served from a
//   process-global in-memory Map seeded in the constructor with 5 hardcoded demo
//   feeds + 10 demo IOCs that EVERY tenant then read — a cross-tenant read-path
//   leak. The matches DB writer (recordThreatMatch) was dormant (0 callers);
//   checkValue wrote in-memory only. All four tables already carry tenant_id and
//   are RLS-enrolled (Chunk B/C); the leak was PURELY the read path.
//
//   The Option-A remediation made every read surface DB-backed + RLS-scoped to
//   the caller's tenant, removed the constructor demo seed (the Maps are now
//   write-only transient state, NOT a read source), wired the dormant matches
//   writer (checkValue/bulkCheck now record a DURABLE tenant-bound threat_matches
//   row), and degraded — not crashed — the deterministic-threat-floor source-IP
//   lookup when no tenant context is available.
//
//   This script proves that closure end-to-end with paired POSITIVE/NEGATIVE
//   legs (Rule 18), a durable match write→read-back leg, a fail-closed leg, a
//   floor-degradation leg, and a DEMO-SEED-GONE leg (a fresh never-seeded tenant
//   sees ZERO — the core of the fix).
//
// WHY THIS SHAPE (non-hollow, per the standing rules)
//   • Driven as the least-privilege RLS-subject role `aegis_app` (NOT the owner,
//     which is BYPASSRLS → a hollow green). Every service read method routes to
//     TiDB.*ForTenant, each of which runs inside withTenantRls(tenantId, …) — the
//     exact aegis_app + tenant-GUC path a request takes. The owner pool is GROUND
//     TRUTH ONLY (proves a foreign-tenant 0 is RLS scoping, not row absence).
//     [Rule 17/18, prod-invariant: aegis_app is the RLS-subject role]
//   • The POSITIVE reads are driven through the REAL service methods the HTTP
//     routes call (threatIntelligence.getFeeds/getIndicators/getRecentMatches/
//     getStats/checkValue/bulkCheck), each given the explicit tenantId the route
//     handler passes from req.tenantId. The route auth gates WHO may call — not
//     WHICH tenant's rows return; the read-path leak is orthogonal to auth and
//     lives entirely in the service+RLS read path these calls exercise verbatim.
//     [route-driven-proof-harness]
//   • Rule 18 (paired): every denial leg (foreign tenant sees 0) is paired with a
//     positive-enforcement leg (owner tenant sees its own rows) in the SAME run,
//     so a green is provably green for its EXACT reason. The substrate rlsPairedProbe
//     additionally confirms the row PHYSICALLY EXISTS under the owner/BYPASSRLS
//     view while the foreign tenant is denied → the 0 is RLS, not absence.
//   • Tenant-qualified physical ids (`<tenantId>::<logicalId>`): A and B seed the
//     SAME deterministic logical rows, but physical ids differ by tenant prefix,
//     so foreign reads against A's ids are clean denials, not same-id collisions.
//   • Seeding is driven through the tenant-bound DB writers (saveFeed /
//     saveIndicator under withTenantRls) — so the rows exist under the same
//     RLS WITH CHECK the request write path uses.
//
// CLEANUP: all rows for the three synthetic run-scoped tenants are deleted across
//   threat_matches, threat_indicators, threat_feeds (by tenant_id). The read/
//   write paths exercised here write NO audit_chain / audit_logs rows.
// ============================================================================

import {
  rlsPairedProbe,
  ownerCount,
  ownerQuery,
  endOwnerPool,
  verdict,
  ProofRun,
  type RlsPairedProbeResult,
} from "./lib/proof-harness";
import { appPool } from "../server/db";
import { threatIntelligence as TI } from "../server/lib/threat-intelligence";
import * as TiDB from "../server/lib/threat-intelligence-db";
import { deterministicThreatFloor } from "../server/lib/deterministic-threat-floor";

const base = Date.now();
const TENANT_A = `rp05-a-${base}`; // owner tenant — seeds + reads its own rows
const TENANT_B = `rp05-b-${base}`; // foreign tenant — must see NONE of A's rows
const TENANT_C = `rp05-c-${base}`; // fresh, NEVER seeded — must see ZERO (demo-seed-gone)

const q = (t: string, l: string): string => `${t}::${l}`;
const A_FEED = q(TENANT_A, "FEED-1");
const A_IOC_IP = q(TENANT_A, "IOC-IP-1");
const A_IOC_DOM = q(TENANT_A, "IOC-DOM-1");
const B_FEED = q(TENANT_B, "FEED-1");
const B_IOC_IP = q(TENANT_B, "IOC-IP-1");

// Match needles (RFC-5737 TEST-NET-2 IP + a .test domain — synthetic, no PII).
const A_IP_VALUE = "198.51.100.7";
const A_DOM_VALUE = "evil-a.test";
const B_IP_VALUE = "203.0.113.9";

const run = new ProofRun(
  "AS/CYBER/READPATH-FIX/THREATINTEL — F-RP-05 read-path cross-tenant proof (feeds + indicators + matches + stats + floor degrade + demo-seed-gone)",
  "DEV",
);

function probeLeg(id: string, label: string, p: RlsPairedProbeResult) {
  run.leg({
    id,
    scenario: `${label} — RLS paired probe as aegis_app (owner=${p.ownerTenant}, foreign=${p.foreignTenant})`,
    expectation:
      "row PHYSICALLY EXISTS (owner/BYPASSRLS) AND owner-tenant SEES it (count=1) AND foreign-tenant DENIED (count=0)",
    observed: `ownerExists=${p.ownerExists} ownerVisible=${p.ownerVisibleCount} foreignVisible=${p.foreignVisibleCount}`,
    verdict: p.pass
      ? verdict.verified(
          "aegis_app RLS-subject read: owner sees its own row, foreign tenant sees 0 of a row proven to exist",
        )
      : verdict.fail(),
    pass: p.pass,
    signals: {
      table: p.table,
      id: p.id,
      ownerExists: p.ownerExists,
      ownerVisibleCount: p.ownerVisibleCount,
      foreignVisibleCount: p.foreignVisibleCount,
    },
  });
}

async function threw(fn: () => Promise<unknown>): Promise<boolean> {
  try {
    await fn();
    return false;
  } catch {
    return true;
  }
}

async function cleanup() {
  const t = [TENANT_A, TENANT_B, TENANT_C];
  await ownerQuery(`DELETE FROM threat_matches WHERE tenant_id = ANY($1::text[])`, [t]);
  await ownerQuery(`DELETE FROM threat_indicators WHERE tenant_id = ANY($1::text[])`, [t]);
  await ownerQuery(`DELETE FROM threat_feeds WHERE tenant_id = ANY($1::text[])`, [t]);
}

async function fatal(msg: string): Promise<never> {
  console.error(`FATAL: ${msg}`);
  await cleanup().catch(() => {});
  await endOwnerPool().catch(() => {});
  await appPool.end().catch(() => {});
  process.exit(1);
}

async function seedFeed(id: string, tenantId: string) {
  await TiDB.saveFeed(
    {
      id,
      name: `feed ${id}`,
      type: "custom",
      url: "https://feed.test/intel",
      enabled: true,
      refreshInterval: 60,
      lastSync: new Date(),
      indicatorCount: 0,
      status: "active",
    },
    tenantId,
  );
}

async function seedIndicator(
  id: string,
  tenantId: string,
  feedId: string,
  type: string,
  value: string,
  severity: string,
  confidence: number,
) {
  const now = new Date();
  await TiDB.saveIndicator(
    {
      id,
      feedId,
      type,
      value,
      severity,
      source: feedId,
      confidence,
      tags: ["vf-rp05", severity],
      description: `vf-rp05 synthetic ${type} indicator`,
      firstSeen: now,
      lastSeen: now,
    },
    tenantId,
  );
}

async function main() {
  console.log("=".repeat(82));
  console.log("VF-RP05 — threat-intel read-path cross-tenant isolation proof (F-RP-05)");
  console.log("ref AS/CYBER/READPATH-FIX/THREATINTEL | env: DEV |", new Date().toISOString());
  console.log(`tenant A (owner)=${TENANT_A}  B (foreign)=${TENANT_B}  C (fresh)=${TENANT_C}`);
  console.log("=".repeat(82));

  await cleanup(); // idempotent pre-clean

  // ── PHASE 1 — SEED via the tenant-bound DB writers (withTenantRls / RLS CHECK) ─
  await seedFeed(A_FEED, TENANT_A);
  await seedIndicator(A_IOC_IP, TENANT_A, A_FEED, "ip", A_IP_VALUE, "critical", 90);
  await seedIndicator(A_IOC_DOM, TENANT_A, A_FEED, "domain", A_DOM_VALUE, "high", 80);
  await seedFeed(B_FEED, TENANT_B);
  await seedIndicator(B_IOC_IP, TENANT_B, B_FEED, "ip", B_IP_VALUE, "medium", 50);

  const aFeedPhys = await ownerCount("threat_feeds", "tenant_id = $1", [TENANT_A]);
  const aIndPhys = await ownerCount("threat_indicators", "tenant_id = $1", [TENANT_A]);
  if (aFeedPhys !== 1 || aIndPhys !== 2) {
    await fatal(`seed invariant broken: A feeds=${aFeedPhys} (want 1), A indicators=${aIndPhys} (want 2)`);
  }
  console.log(`\nseeded: A feed=${A_FEED} +2 IOCs;  B feed=${B_FEED} +1 IOC`);

  // ── PHASE 2 — SUBSTRATE: per-table paired RLS probe (RLS-enrolled tables) ─────
  const pFeed = await rlsPairedProbe("threat_feeds", A_FEED, TENANT_A, TENANT_B);
  const pInd = await rlsPairedProbe("threat_indicators", A_IOC_IP, TENANT_A, TENANT_B);
  probeLeg("RP-FEED", "threat_feeds", pFeed);
  probeLeg("RP-IND", "threat_indicators", pInd);

  // ── PHASE 3 — SVC-POS: tenant A drives the real read methods ──────────────────
  const feedsA = await TI.getFeeds(TENANT_A);
  const indsA = await TI.getIndicators(TENANT_A);
  const statsA = await TI.getStats(TENANT_A);
  const feedsAWellFormed =
    feedsA.length === 1 &&
    feedsA[0].id === A_FEED &&
    feedsA[0].apiKey === undefined && // encrypted key NEVER surfaced through read path
    feedsA[0].type === "custom" &&
    feedsA[0].status === "active";
  const indsAWellFormed =
    indsA.length === 2 &&
    indsA.every((i) => i.id.startsWith(`${TENANT_A}::`)) &&
    indsA.every((i) => Array.isArray(i.tags) && i.tags.includes("vf-rp05")) && // JSON tags parsed
    indsA.some((i) => i.id === A_IOC_IP && i.severity === "critical");
  const statsAPos =
    statsA.totalIndicators === 2 && // REAL per-tenant count, not a fabricated per-feed SUM
    statsA.activeFeeds === 1 &&
    statsA.criticalIndicators === 1 &&
    statsA.matchesToday === 0; // none recorded yet
  const posPass = feedsAWellFormed && indsAWellFormed && statsAPos;
  run.leg({
    id: "SVC-POS",
    scenario:
      "POSITIVE — tenant A drives getFeeds/getIndicators/getStats (real service path, aegis_app, explicit tenantId)",
    expectation:
      "A sees its own 1 feed (apiKey hidden) + 2 well-formed indicators (tags parsed); stats are the REAL per-tenant counts (2 indicators / 1 active feed / 1 critical / 0 matches)",
    observed:
      `feeds=${feedsA.length} feedWellFormed=${feedsAWellFormed} inds=${indsA.length} indWellFormed=${indsAWellFormed} ` +
      `stats(tot/active/crit/today)=${statsA.totalIndicators}/${statsA.activeFeeds}/${statsA.criticalIndicators}/${statsA.matchesToday}`,
    verdict: posPass
      ? verdict.verified(
          "the real route-backing read methods return A's own rows with correctly-mapped DTOs and real per-tenant stats",
        )
      : verdict.fail(),
    pass: posPass,
    signals: {
      feeds: feedsA.length,
      feedApiKeyHidden: feedsA[0]?.apiKey === undefined,
      indicators: indsA.length,
      totalIndicators: statsA.totalIndicators,
      activeFeeds: statsA.activeFeeds,
      criticalIndicators: statsA.criticalIndicators,
      matchesToday: statsA.matchesToday,
    },
  });

  // ── PHASE 4 — DURABLE MATCH write→read-back (dormant writer now wired) ─────────
  // checkValue(A) + bulkCheck(A) each record a DURABLE tenant-bound threat_matches
  // row; getRecentMatches(A) reads them back (a SEPARATE withTenantRls tx → proves
  // durable-before-read, not in-memory). Risk-score round-trip: service 0–100 →
  // stored 0–1 fraction numeric(4,3) → read back ×100, exact for integer scores.
  const ipMatch = await TI.checkValue(A_IP_VALUE, TENANT_A, "vf-rp05-ip");
  const bulk = await TI.bulkCheck([{ value: A_DOM_VALUE, context: "vf-rp05-dom" }], TENANT_A);
  const matchesA = await TI.getRecentMatches(TENANT_A, 50);
  const aMatchPhys = await ownerCount("threat_matches", "tenant_id = $1", [TENANT_A]);
  const storedRows = await ownerQuery<{ risk_score: string }>(
    `SELECT risk_score FROM threat_matches WHERE tenant_id = $1 AND indicator_id = $2 ORDER BY matched_at DESC LIMIT 1`,
    [TENANT_A, A_IOC_IP],
  );
  const storedFraction = storedRows[0]?.risk_score;
  const expectedFraction =
    ipMatch != null
      ? (Math.max(0, Math.min(100, ipMatch.riskScore)) / 100).toFixed(3)
      : "MISSING";
  const readBackIp = matchesA.find((m) => m.matchedValue === A_IP_VALUE);
  const matchPass =
    !!ipMatch &&
    ipMatch.indicator.id === A_IOC_IP &&
    Number.isInteger(ipMatch.riskScore) &&
    ipMatch.riskScore >= 0 &&
    ipMatch.riskScore <= 100 &&
    bulk.length === 1 &&
    bulk[0].indicator.id === A_IOC_DOM &&
    matchesA.length === 2 &&
    aMatchPhys === 2 &&
    storedFraction === expectedFraction && // 0–100 → 0–1 fraction round-trip sound
    !!readBackIp &&
    readBackIp.riskScore === ipMatch.riskScore; // scaled back ×100 to the same integer
  run.leg({
    id: "MATCH-DURABLE",
    scenario:
      "DURABLE MATCH — tenant A checkValue + bulkCheck record durable threat_matches rows, read back via getRecentMatches (aegis_app)",
    expectation:
      "checkValue/bulkCheck hit A's indicators, persist exactly 2 tenant-A threat_matches rows, and getRecentMatches reads them back with the risk-score round-trip (0–100 → numeric(4,3) fraction → ×100) intact",
    observed:
      `ipMatch=${ipMatch ? ipMatch.riskScore : "null"} bulk=${bulk.length} recent=${matchesA.length} ` +
      `physRows=${aMatchPhys} storedFraction=${storedFraction} expectedFraction=${expectedFraction} ` +
      `readBackRisk=${readBackIp?.riskScore}`,
    verdict: matchPass
      ? verdict.verified(
          "the dormant matches writer is wired: checkValue/bulkCheck persist durable tenant-bound rows that read back through the service with a sound risk-score round-trip",
        )
      : verdict.fail(),
    pass: matchPass,
    signals: {
      ipMatchRisk: ipMatch?.riskScore,
      bulkMatches: bulk.length,
      recentMatches: matchesA.length,
      physMatchRows: aMatchPhys,
      storedFraction,
      expectedFraction,
      readBackRisk: readBackIp?.riskScore,
    },
  });

  // capture A's match id for the cross-tenant substrate probe
  const idRows = await ownerQuery<{ id: string }>(
    `SELECT id FROM threat_matches WHERE tenant_id = $1 AND indicator_id = $2 ORDER BY matched_at DESC LIMIT 1`,
    [TENANT_A, A_IOC_IP],
  );
  const aMatchId = idRows[0]?.id;
  if (!aMatchId) await fatal("could not capture A's threat_matches id for the cross-tenant probe");
  const pMatch = await rlsPairedProbe("threat_matches", aMatchId!, TENANT_A, TENANT_B);
  probeLeg("RP-MATCH", "threat_matches", pMatch);

  // ── PHASE 5 — SVC-NEG: tenant B drives the SAME read methods ───────────────────
  const feedsB = await TI.getFeeds(TENANT_B);
  const indsB = await TI.getIndicators(TENANT_B);
  const matchesB = await TI.getRecentMatches(TENANT_B, 50);
  const statsB = await TI.getStats(TENANT_B);
  const bSeesAFeed = feedsB.some((f) => f.id.startsWith(`${TENANT_A}::`));
  const bSeesAInd = indsB.some((i) => i.id.startsWith(`${TENANT_A}::`));
  const bSeesAMatch = matchesB.some(
    (m) => m.matchedValue === A_IP_VALUE || m.matchedValue === A_DOM_VALUE,
  );
  const negPass =
    feedsB.length === 1 &&
    feedsB[0].id === B_FEED && // B sees its OWN feed (paired positive for B)
    !bSeesAFeed &&
    indsB.length === 1 &&
    indsB[0].id === B_IOC_IP &&
    !bSeesAInd &&
    matchesB.length === 0 &&
    !bSeesAMatch &&
    statsB.totalIndicators === 1 && // B's own count, NOT A's 2 and NOT a fabricated seed
    pFeed.foreignVisibleCount === 0 &&
    pInd.foreignVisibleCount === 0 &&
    pMatch.foreignVisibleCount === 0 &&
    aFeedPhys >= 1 &&
    aIndPhys >= 1 &&
    aMatchPhys >= 1; // A's rows physically exist → B's 0 is RLS, not absence
  run.leg({
    id: "SVC-NEG",
    scenario:
      "NEGATIVE — tenant B drives getFeeds/getIndicators/getRecentMatches/getStats (aegis_app, GUC=B)",
    expectation:
      "B sees its OWN 1 feed / 1 indicator / 0 matches and NONE of A's via the real methods; raw aegis_app probe agrees (foreign count 0) while A's rows physically exist → RLS-caused",
    observed:
      `B.feeds=${feedsB.length} B.seesAFeed=${bSeesAFeed} B.inds=${indsB.length} B.seesAInd=${bSeesAInd} ` +
      `B.matches=${matchesB.length} B.seesAMatch=${bSeesAMatch} B.totalInd=${statsB.totalIndicators} ` +
      `rawForeign(feed/ind/match)=${pFeed.foreignVisibleCount}/${pInd.foreignVisibleCount}/${pMatch.foreignVisibleCount} ` +
      `A.phys(feed/ind/match)=${aFeedPhys}/${aIndPhys}/${aMatchPhys}`,
    verdict: negPass
      ? verdict.verified(
          "cross-tenant read CLOSED: foreign tenant's real service reads return none of A's feeds/indicators/matches; service result and raw aegis_app probe agree → boundary is RLS",
        )
      : verdict.fail(),
    pass: negPass,
    signals: {
      bFeeds: feedsB.length,
      bSeesAFeed,
      bInds: indsB.length,
      bSeesAInd,
      bMatches: matchesB.length,
      bSeesAMatch,
      bTotalInd: statsB.totalIndicators,
      rawForeignFeed: pFeed.foreignVisibleCount,
      rawForeignInd: pInd.foreignVisibleCount,
      rawForeignMatch: pMatch.foreignVisibleCount,
    },
  });

  // ── PHASE 6 — FAIL-CLOSED: empty tenant on every read + write surface ─────────
  const fcGetFeeds = await threw(() => TI.getFeeds(""));
  const fcGetInds = await threw(() => TI.getIndicators(""));
  const fcGetMatches = await threw(() => TI.getRecentMatches("", 10));
  const fcGetStats = await threw(() => TI.getStats(""));
  const fcCheck = await threw(() => TI.checkValue(A_IP_VALUE, "", "fc"));
  const fcBulk = await threw(() => TI.bulkCheck([{ value: A_IP_VALUE }], ""));
  const failClosedPass =
    fcGetFeeds && fcGetInds && fcGetMatches && fcGetStats && fcCheck && fcBulk;
  run.leg({
    id: "FAIL-CLOSED",
    scenario: "FAIL-CLOSED — every threat-intel read/write surface with an empty tenantId",
    expectation:
      "getFeeds/getIndicators/getRecentMatches/getStats/checkValue/bulkCheck ALL throw on an empty tenant — never a tenant-blind read or an unstamped write",
    observed:
      `feeds=${fcGetFeeds} inds=${fcGetInds} matches=${fcGetMatches} stats=${fcGetStats} check=${fcCheck} bulk=${fcBulk}`,
    verdict: failClosedPass
      ? verdict.verified("all six surfaces fail closed (throw) when tenant context is absent")
      : verdict.fail(),
    pass: failClosedPass,
    signals: { fcGetFeeds, fcGetInds, fcGetMatches, fcGetStats, fcCheck, fcBulk },
  });

  // ── PHASE 7 — FLOOR DEGRADE (paired): degrade, not crash, on missing tenant ───
  // The demo-IOC removal must DEGRADE the deterministic-threat-floor source-IP
  // lookup. NEGATIVE: no tenant → degraded signal, no crash. POSITIVE: with a
  // tenant → no no_tenant_context signal (it attempted the scoped lookup), no crash.
  const floorCtx = { eventType: "login_anomaly", sourceIP: "198.51.100.250" };
  const floorNoTenant = await deterministicThreatFloor(floorCtx);
  const floorWithTenant = await deterministicThreatFloor(floorCtx, { tenantId: TENANT_A });
  const degradeNeg =
    floorNoTenant.degradedSignals.includes("source_ip_threat_intel:no_tenant_context") &&
    Number.isFinite(floorNoTenant.score);
  const degradePos =
    !floorWithTenant.degradedSignals.includes("source_ip_threat_intel:no_tenant_context") &&
    Number.isFinite(floorWithTenant.score);
  const floorPass = degradeNeg && degradePos;
  run.leg({
    id: "FLOOR-DEGRADE",
    scenario:
      "FLOOR DEGRADE — deterministicThreatFloor with no tenant (degrade) vs with a tenant (scoped lookup), paired",
    expectation:
      "no-tenant call emits 'source_ip_threat_intel:no_tenant_context' and still returns a finite score (degrades, never crashes); the tenant-scoped call does NOT emit that signal and also returns a finite score",
    observed:
      `noTenant.signals=[${floorNoTenant.degradedSignals.join(",")}] noTenant.score=${floorNoTenant.score} ` +
      `withTenant.signals=[${floorWithTenant.degradedSignals.join(",")}] withTenant.score=${floorWithTenant.score}`,
    verdict: floorPass
      ? verdict.verified(
          "demo-IOC removal degrades the floor's source-IP lookup (named signal, finite score) instead of crashing; tenant-scoped path runs the lookup cleanly",
        )
      : verdict.fail(),
    pass: floorPass,
    signals: {
      noTenantHasSignal: degradeNeg,
      noTenantScore: floorNoTenant.score,
      withTenantNoSignal: degradePos,
      withTenantScore: floorWithTenant.score,
    },
  });

  // ── PHASE 8 — DEMO-SEED-GONE: a fresh never-seeded tenant sees ZERO ───────────
  // The core of the F-RP-05 fix: the constructor no longer seeds 5 demo feeds +
  // 10 demo IOCs into a process-global Map. A brand-new tenant therefore reads
  // empty — not the old hardcoded demo set every tenant used to see.
  const feedsC = await TI.getFeeds(TENANT_C);
  const indsC = await TI.getIndicators(TENANT_C);
  const matchesC = await TI.getRecentMatches(TENANT_C, 50);
  const statsC = await TI.getStats(TENANT_C);
  const demoGonePass =
    feedsC.length === 0 &&
    indsC.length === 0 &&
    matchesC.length === 0 &&
    statsC.totalIndicators === 0 &&
    statsC.activeFeeds === 0 &&
    statsC.matchesToday === 0;
  run.leg({
    id: "DEMO-SEED-GONE",
    scenario:
      "DEMO-SEED-GONE — a fresh never-seeded tenant C drives every read surface (aegis_app)",
    expectation:
      "C sees 0 feeds / 0 indicators / 0 matches and all-zero stats — proving no process-global demo seed (the old 5 feeds + 10 IOCs) leaks to any tenant",
    observed:
      `C.feeds=${feedsC.length} C.inds=${indsC.length} C.matches=${matchesC.length} ` +
      `C.stats(tot/active/today)=${statsC.totalIndicators}/${statsC.activeFeeds}/${statsC.matchesToday}`,
    verdict: demoGonePass
      ? verdict.verified(
          "the process-global demo seed is gone: a fresh tenant reads empty across feeds/indicators/matches/stats",
        )
      : verdict.fail(),
    pass: demoGonePass,
    signals: {
      cFeeds: feedsC.length,
      cInds: indsC.length,
      cMatches: matchesC.length,
      cTotalInd: statsC.totalIndicators,
    },
  });

  // ── REPORT + cleanup ──────────────────────────────────────────────────────────
  run.report();
  await run.writeJson().catch(() => {});

  await cleanup();
  const leftover =
    (await ownerCount("threat_matches", "tenant_id = ANY($1::text[])", [[TENANT_A, TENANT_B, TENANT_C]])) +
    (await ownerCount("threat_indicators", "tenant_id = ANY($1::text[])", [[TENANT_A, TENANT_B, TENANT_C]])) +
    (await ownerCount("threat_feeds", "tenant_id = ANY($1::text[])", [[TENANT_A, TENANT_B, TENANT_C]]));
  console.log(`\ncleanup: synthetic rows remaining across 3 tenants = ${leftover} (want 0)`);

  await endOwnerPool().catch(() => {});
  await appPool.end().catch(() => {});

  const { allPass } = run.acceptance();
  process.exit(allPass && leftover === 0 ? 0 : 1);
}

main().catch(async (e) => {
  await fatal(e instanceof Error ? e.stack || e.message : String(e));
});
