// ============================================================================
// VF-RP03 — Entity-Resolution read-path cross-tenant isolation proof (F-RP-03)
// ----------------------------------------------------------------------------
// ref AS/AEGIS-CYBER/VERIFY/2026/001 | env: DEV | HOLD at publish gate
//
// WHAT THIS PROVES
//   F-RP-03 (HIGH): the ER read path served entities/links/clusters from
//   process-global Maps that were ALSO the matching index for clustering, so the
//   read path AND the matching/clustering surface were NOT tenant-scoped. The
//   operator fix made all three tables DB-PRIMARY (Maps gone) and tenant-scoped:
//     • er_entities / er_clusters are RLS-enrolled (tenant_id) → scoped by the
//       request-tx `app.current_tenant_id` GUC.
//     • er_entity_links has NO tenant_id (parent-scoped sibling, mirrors
//       soar_action_results) → bounded at the SQL level by `loadTenantLinks`,
//       whose two correlated EXISTS subqueries require BOTH endpoints visible in
//       the RLS-enrolled er_entities under the caller's tenant.
//   This script proves that closure end-to-end, ONE proof covering all three
//   tables + the live clustering/matching surface (detectClusters), with paired
//   POSITIVE/NEGATIVE legs, a §3.5 match-boundedness leg (B's clustering never
//   pulls in A), a cross-tenant WRITE-rejection leg (createLink gate), and an
//   empty-tenant fail-closed leg.
//
// 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). The owner pool is GROUND TRUTH ONLY
//     (proves a foreign-tenant 0 is RLS/SQL scoping, not row absence). [Rule 17/18]
//   • The seed and the POSITIVE reads are driven through the REAL service methods
//     the HTTP routes call (detectClusters / getEntityResolutionDashboard /
//     getAllClusters / getCluster / resolveEntity / generateGraphVisualization /
//     createLink), inside the SAME request runtime the middleware builds: a pinned
//     aegis_app client + a real Drizzle tx + the LOCAL tenant GUC + the
//     tenantContext ALS. So the service's `db` proxy resolves to the aegis_app tx
//     client exactly as in a request. (The ER routes are role-gated; route auth
//     gates WHO may call — not WHICH tenant's rows are returned. The read-path
//     leak is orthogonal to auth and lives entirely in the service+RLS+SQL read
//     path these calls exercise verbatim.) [route-driven-proof-harness]
//   • Rule 18 (paired): every denial leg (foreign tenant sees 0 / cannot write /
//     clustering excludes A) is paired with a positive-enforcement leg (owner
//     tenant sees its own rows / can write / clusters its own graph), in the same
//     run, so a green is provably green for its EXACT reason.
//   • "0 is RLS/SQL, not a JS filter": loadTenantLinks applies NO app-side tenant
//     filter (the bound is the SQL EXISTS join). The LINK-BOUND leg shows that
//     under GUC=B a RAW SELECT of A's link row (er_entity_links has NO RLS) DOES
//     return it, while loadTenantLinks() returns 0 of A's — so the boundary is the
//     both-endpoint EXISTS join, not RLS-on-the-table and not app code.
//   • Tenant-qualified physical ids (`<tenantId>::<logicalId>`): A and B each seed
//     the SAME deterministic logical graph, but the physical ids differ by tenant
//     prefix, so foreign reads/writes against A's ids are clean denials, not
//     same-id collisions.
//
// CLEANUP: all rows for the two synthetic run-scoped tenants are deleted across
//   er_entity_links (by id/endpoint prefix), er_clusters + er_entities (by
//   tenant_id). No audit rows are written by the service paths exercised here
//   (the /detect route's audit log is NOT driven — the read-path leak lives in the
//   service path, which this drives directly, same precedent as vf-rp02).
// ============================================================================

import { sql } from "drizzle-orm";
import { appPool, tenantContext, makeClientRunner, type DrizzleDb } from "../server/db";
import * as ER from "../server/lib/entity-resolution";
import * as ErDB from "../server/lib/entity-resolution-db";
import {
  rlsPairedProbe,
  ownerCount,
  ownerQuery,
  endOwnerPool,
  verdict,
  ProofRun,
  type RlsPairedProbeResult,
} from "./lib/proof-harness";

const base = Date.now();
const TENANT_A = `rp03-a-${base}`; // owner tenant — seeds + reads its own rows
const TENANT_B = `rp03-b-${base}`; // foreign tenant — must see/cluster/write NONE of A's

const q = (t: string, l: string): string => `${t}::${l}`;
const A_ENT = q(TENANT_A, "ENT-ACC-1");
const A_CLUSTER = q(TENANT_A, "CLUSTER-1");
const A_LINK1 = q(TENANT_A, "LINK-1");

/** FAITHFUL request runtime (the committing variant): a pinned aegis_app client
 *  + a real Drizzle tx + the LOCAL tenant GUC, with the PgTransaction stashed in
 *  the same AsyncLocalStorage the middleware uses, so the service's `db` proxy
 *  resolves to THIS connection under RLS — the exact production mechanism. */
async function runRequestPhaseTx(
  tenantId: string,
  fn: (tx: DrizzleDb) => Promise<void>,
): Promise<"COMMIT" | "ROLLBACK"> {
  const client = await appPool.connect();
  try {
    const tdb = makeClientRunner(client);
    await tdb.transaction(async (tx) => {
      await tx.execute(
        sql`SELECT set_config('app.current_tenant_id', ${tenantId}, true)`,
      );
      await tenantContext.run(
        { runner: tx as unknown as DrizzleDb, tenantId },
        () => fn(tx as unknown as DrizzleDb),
      );
    });
    return "COMMIT";
  } finally {
    client.release();
  }
}

const run = new ProofRun(
  "AS/CYBER/READPATH-FIX/ER — F-RP-03 read-path cross-tenant proof (entities + clusters + links + clustering)",
  "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 cleanup() {
  await ownerQuery(
    `DELETE FROM er_entity_links
       WHERE source_id LIKE $1 OR source_id LIKE $2 OR target_id LIKE $1 OR target_id LIKE $2`,
    [`${TENANT_A}::%`, `${TENANT_B}::%`],
  );
  await ownerQuery(`DELETE FROM er_clusters WHERE tenant_id = $1 OR tenant_id = $2`, [
    TENANT_A,
    TENANT_B,
  ]);
  await ownerQuery(`DELETE FROM er_entities WHERE tenant_id = $1 OR tenant_id = $2`, [
    TENANT_A,
    TENANT_B,
  ]);
}

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 main() {
  console.log("=".repeat(82));
  console.log(
    "VF-RP03 — ER read-path cross-tenant isolation proof (F-RP-03, all three tables + clustering)",
  );
  console.log(
    "ref AS/AEGIS-CYBER/VERIFY/2026/001 | env: DEV |",
    new Date().toISOString(),
  );
  console.log(`tenant A (owner)=${TENANT_A}  tenant B (foreign)=${TENANT_B}`);
  console.log("=".repeat(82));

  await cleanup(); // idempotent pre-clean

  // ── PHASE 1 — SEED tenant-A via the REAL service path (detectClusters) ────────
  // detectClusters(A) triggers ensureSeeded(A) (8 entities + 10 links, all
  // tenant-qualified + tenant-stamped) then computes A's clusters, all inside A's
  // request tx (commits). This is the live matching/clustering surface.
  let clustersA: ER.EntityCluster[] = [];
  await runRequestPhaseTx(TENANT_A, async () => {
    clustersA = await ER.detectClusters({ tenantId: TENANT_A });
  });
  if (clustersA.length !== 1 || clustersA[0]?.id !== A_CLUSTER) {
    await fatal(
      `seed/cluster invariant broken: expected exactly 1 cluster id=${A_CLUSTER}, got ${clustersA
        .map((c) => c.id)
        .join(",")}`,
    );
  }

  console.log(
    `\nseeded A: cluster=${A_CLUSTER} members=${clustersA[0].entities.length} ` +
      `(entity probe=${A_ENT})`,
  );

  // ── PHASE 2 — SUBSTRATE: per-table paired RLS probe (RLS-enrolled tables) ─────
  const pEnt = await rlsPairedProbe("er_entities", A_ENT, TENANT_A, TENANT_B);
  const pCluster = await rlsPairedProbe("er_clusters", A_CLUSTER, TENANT_A, TENANT_B);
  probeLeg("RP-ENT", "er_entities", pEnt);
  probeLeg("RP-CLUSTER", "er_clusters", pCluster);

  // ── PHASE 3 — PROVE-THROUGH-ROUTE: real service read methods, paired A vs B ──
  // POSITIVE (tenant A, as aegis_app): the exact methods the routes call return
  // A's rows with WELL-FORMED DTOs.
  let dashA: Awaited<ReturnType<typeof ER.getEntityResolutionDashboard>> | undefined;
  let allClustersA: ER.EntityCluster[] = [];
  let clusterA: ER.EntityCluster | undefined;
  let resolveA: Awaited<ReturnType<typeof ER.resolveEntity>>;
  let graphA: ER.GraphVisualization | undefined;
  await runRequestPhaseTx(TENANT_A, async () => {
    dashA = await ER.getEntityResolutionDashboard({ tenantId: TENANT_A });
    allClustersA = await ER.getAllClusters({ tenantId: TENANT_A });
    clusterA = await ER.getCluster(A_CLUSTER, { tenantId: TENANT_A });
    resolveA = await ER.resolveEntity(A_ENT, { tenantId: TENANT_A });
    graphA = await ER.generateGraphVisualization(undefined, { tenantId: TENANT_A });
  });

  // NEGATIVE (tenant B, as aegis_app): B seeds its OWN graph; the same methods
  // must return NONE of A's rows.
  let dashBSeesA = false;
  let allClustersBSeesA = false;
  let clusterBById: ER.EntityCluster | undefined;
  let resolveBById: Awaited<ReturnType<typeof ER.resolveEntity>>;
  let graphBSeesA = false;
  await runRequestPhaseTx(TENANT_B, async () => {
    const dashB = await ER.getEntityResolutionDashboard({ tenantId: TENANT_B });
    dashBSeesA = dashB.recentClusters.some((c) => c.id === A_CLUSTER);
    const acB = await ER.getAllClusters({ tenantId: TENANT_B });
    allClustersBSeesA = acB.some((c) => c.id === A_CLUSTER);
    clusterBById = await ER.getCluster(A_CLUSTER, { tenantId: TENANT_B });
    resolveBById = await ER.resolveEntity(A_ENT, { tenantId: TENANT_B });
    const graphB = await ER.generateGraphVisualization(undefined, { tenantId: TENANT_B });
    graphBSeesA = graphB.nodes.some((n) => n.id.startsWith(`${TENANT_A}::`));
  });

  // ground truth: A's rows physically still exist while B's service read sees 0.
  const aRowsEnt = await ownerCount("er_entities", "id = $1", [A_ENT]);
  const aRowsCluster = await ownerCount("er_clusters", "id = $1", [A_CLUSTER]);

  // SVC-POS — A's service read returns A's rows with well-formed DTOs.
  const clusterWellFormed =
    !!clusterA &&
    clusterA.id === A_CLUSTER &&
    Array.isArray(clusterA.entities) &&
    clusterA.entities.length === clustersA[0].entities.length &&
    clusterA.entities.every((e) => e.startsWith(`${TENANT_A}::`)) &&
    typeof clusterA.riskLevel === "string";
  const resolveWellFormed =
    !!resolveA &&
    resolveA.entity.id === A_ENT &&
    resolveA.entity.type === "ACCOUNT" &&
    resolveA.directLinks.length > 0 &&
    resolveA.directLinks.every(
      (l) => l.sourceId.startsWith(`${TENANT_A}::`) && l.targetId.startsWith(`${TENANT_A}::`),
    );
  const posPass =
    !!dashA &&
    dashA.totalEntities === 8 &&
    dashA.totalLinks === 10 &&
    dashA.totalClusters === 1 &&
    allClustersA.some((c) => c.id === A_CLUSTER) &&
    clusterWellFormed &&
    resolveWellFormed &&
    !!graphA &&
    graphA.nodes.length === 8 &&
    graphA.edges.length === 10 &&
    graphA.nodes.every((n) => n.id.startsWith(`${TENANT_A}::`));
  run.leg({
    id: "SVC-POS",
    scenario:
      "POSITIVE — tenant A drives dashboard/getAllClusters/getCluster/resolveEntity/graph (real service path, aegis_app)",
    expectation:
      "A sees its own 8 entities / 10 links / 1 cluster, well-formed DTOs, all ids tenant-A-qualified",
    observed:
      `dash(ent/links/clusters)=${dashA?.totalEntities}/${dashA?.totalLinks}/${dashA?.totalClusters} ` +
      `clusterWellFormed=${clusterWellFormed} resolveWellFormed=${resolveWellFormed} ` +
      `graph(nodes/edges)=${graphA?.nodes.length}/${graphA?.edges.length}`,
    verdict: posPass
      ? verdict.verified(
          "the real route-backing read methods return A's own rows with correctly-mapped, tenant-qualified DTOs",
        )
      : verdict.fail(),
    pass: posPass,
    signals: {
      dashEntities: dashA?.totalEntities,
      dashLinks: dashA?.totalLinks,
      dashClusters: dashA?.totalClusters,
      clusterMembers: clusterA?.entities.length,
      resolveDirectLinks: resolveA?.directLinks.length,
      graphNodes: graphA?.nodes.length,
      graphEdges: graphA?.edges.length,
    },
  });

  // SVC-NEG — B's service read returns NONE of A's rows, while A's rows are proven
  // to physically exist (so the 0 is RLS scoping, not absence).
  const negPass =
    !dashBSeesA &&
    !allClustersBSeesA &&
    clusterBById === undefined &&
    resolveBById === undefined &&
    !graphBSeesA &&
    pEnt.foreignVisibleCount === 0 &&
    pCluster.foreignVisibleCount === 0 &&
    aRowsEnt >= 1 &&
    aRowsCluster >= 1;
  run.leg({
    id: "SVC-NEG",
    scenario:
      "NEGATIVE — tenant B drives the SAME service read methods (aegis_app, GUC=B)",
    expectation:
      "B sees 0 of A's entity/cluster via the real methods; A's rows still physically exist; raw aegis_app SELECT agrees (0) → RLS-caused, not a JS filter",
    observed:
      `B.dashSeesA=${dashBSeesA} B.allClustersSeesA=${allClustersBSeesA} ` +
      `B.clusterById=${clusterBById === undefined ? "undefined" : "FOUND"} ` +
      `B.resolveById=${resolveBById === undefined ? "undefined" : "FOUND"} B.graphSeesA=${graphBSeesA} ` +
      `rawForeignEnt=${pEnt.foreignVisibleCount} rawForeignCluster=${pCluster.foreignVisibleCount} ` +
      `A.physRows(ent/cluster)=${aRowsEnt}/${aRowsCluster}`,
    verdict: negPass
      ? verdict.verified(
          "cross-tenant read CLOSED: foreign tenant's real service read returns none of A's rows; service result and raw aegis_app SELECT agree → boundary is RLS",
        )
      : verdict.fail(),
    pass: negPass,
    signals: {
      dashBSeesA,
      allClustersBSeesA,
      clusterByIdFound: clusterBById !== undefined,
      resolveByIdFound: resolveBById !== undefined,
      graphBSeesA,
      rawForeignEnt: pEnt.foreignVisibleCount,
      rawForeignCluster: pCluster.foreignVisibleCount,
      aPhysRowsEnt: aRowsEnt,
      aPhysRowsCluster: aRowsCluster,
    },
  });

  // ── PHASE 4 — LINK-BOUNDEDNESS (er_entity_links has NO RLS; prove EXISTS bound)
  // er_entity_links is NOT RLS-enrolled, so the boundary is the both-endpoint
  // EXISTS join in loadTenantLinks, NOT RLS-on-the-table. Under GUC=A the bounded
  // loader returns A's 10 links; under GUC=B it returns 0 of A's — while a RAW
  // SELECT (no EXISTS) of A's link under GUC=B DOES return it (table has no RLS),
  // proving the bound is the SQL join, not the table and not an app filter.
  let linksA: string[] = [];
  await runRequestPhaseTx(TENANT_A, async () => {
    linksA = (await ErDB.loadTenantLinks()).map((l) => l.id);
  });
  let linksBSeesA = false;
  let rawBSeesALink = 0;
  await runRequestPhaseTx(TENANT_B, async () => {
    const bLinks = (await ErDB.loadTenantLinks()).map((l) => l.id);
    linksBSeesA = bLinks.some((id) => id.startsWith(`${TENANT_A}::`));
    const raw = (await (await import("../server/db")).db.execute(
      sql`SELECT count(*)::int AS n FROM er_entity_links WHERE id = ${A_LINK1}`,
    )) as unknown as { rows: Array<{ n: number }> };
    rawBSeesALink = Number(raw.rows[0]?.n ?? 0);
  });
  const aLinksPhys = await ownerCount("er_entity_links", "id LIKE $1", [`${TENANT_A}::%`]);
  const linkBoundPass =
    linksA.length === 10 &&
    linksA.every((id) => id.startsWith(`${TENANT_A}::`)) &&
    !linksBSeesA &&
    rawBSeesALink === 1 && // raw SELECT (no EXISTS) sees A's link → table has no RLS
    aLinksPhys === 10; // A's links physically exist
  run.leg({
    id: "LINK-BOUND",
    scenario:
      "LINK BOUNDEDNESS — loadTenantLinks both-endpoint EXISTS bound vs raw SELECT (aegis_app, GUC=A then GUC=B)",
    expectation:
      "A's bounded loader returns A's 10 links; B's bounded loader returns 0 of A's; a RAW SELECT under GUC=B still sees A's link (no RLS on table) → bound is the SQL EXISTS join, not RLS-on-table/app-filter",
    observed:
      `A.boundedLinks=${linksA.length} B.boundedSeesA=${linksBSeesA} ` +
      `B.rawSeesALink=${rawBSeesALink} A.physLinks=${aLinksPhys}`,
    verdict: linkBoundPass
      ? verdict.verified(
          "link cross-tenant CLOSED by the both-endpoint EXISTS join: B's bounded read returns none of A's links though they physically exist and the table carries no RLS",
        )
      : verdict.fail(),
    pass: linkBoundPass,
    signals: { aBoundedLinks: linksA.length, linksBSeesA, rawBSeesALink, aLinksPhys },
  });

  // ── PHASE 5 — §3.5 MATCH-BOUNDEDNESS: B-detectClusters excludes A ─────────────
  // The live matching surface. B's connected-components walk must build ONLY from
  // B's RLS-visible entities + B's both-endpoint-owned links — never pulling in A.
  let clustersB: ER.EntityCluster[] = [];
  await runRequestPhaseTx(TENANT_B, async () => {
    clustersB = await ER.detectClusters({ tenantId: TENANT_B });
  });
  const bClusterMembers = clustersB.flatMap((c) => c.entities);
  const matchPass =
    clustersB.length === 1 &&
    clustersB.every((c) => c.id.startsWith(`${TENANT_B}::`)) &&
    bClusterMembers.length > 0 &&
    bClusterMembers.every((id) => id.startsWith(`${TENANT_B}::`)) &&
    !bClusterMembers.some((id) => id.startsWith(`${TENANT_A}::`));
  run.leg({
    id: "MATCH-BOUND",
    scenario:
      "§3.5 — tenant B runs detectClusters (the live matching surface, real service path, aegis_app)",
    expectation:
      "B clusters its OWN graph (cluster id + every member tenant-B-qualified) and NO member is a tenant-A entity → matching/clustering is tenant-bounded",
    observed:
      `B.clusters=${clustersB.length} B.members=${bClusterMembers.length} ` +
      `allB=${bClusterMembers.every((id) => id.startsWith(`${TENANT_B}::`))} ` +
      `anyA=${bClusterMembers.some((id) => id.startsWith(`${TENANT_A}::`))}`,
    verdict: matchPass
      ? verdict.verified(
          "match-boundedness CLOSED: B's clustering builds only from B's tenant-visible graph; A's entities/links never enter B's connected components",
        )
      : verdict.fail(),
    pass: matchPass,
    signals: {
      bClusters: clustersB.length,
      bMembers: bClusterMembers.length,
      bClusterIds: clustersB.map((c) => c.id),
    },
  });

  // ── PHASE 6 — CROSS-TENANT WRITE REJECTION (createLink gate, write authenticity)
  // NEGATIVE: B attempts createLink with A's entity as the source. The
  // gate-before-write (getEntity under RLS) cannot see A's entity → whole op
  // REFUSED (returns undefined), and NO row is written.
  let bWriteResult: ER.EntityLink | undefined = undefined;
  await runRequestPhaseTx(TENANT_B, async () => {
    bWriteResult = await ER.createLink(
      A_ENT, // tenant A's entity — not visible to B
      q(TENANT_B, "ENT-ACC-1"),
      "TRANSACTION",
      ["rp03 cross-tenant write probe"],
      { tenantId: TENANT_B },
    );
  });
  const bForeignLinkRows = await ownerCount(
    "er_entity_links",
    "source_id = $1 AND target_id = $2",
    [A_ENT, q(TENANT_B, "ENT-ACC-1")],
  );
  const writeNegPass = bWriteResult === undefined && bForeignLinkRows === 0;
  run.leg({
    id: "WRITE-NEG",
    scenario:
      "NEGATIVE — tenant B attempts createLink using A's entity as source (real service path, aegis_app)",
    expectation:
      "createLink is REFUSED (returns undefined at the gate-before-write) AND no er_entity_links row is written for the cross-tenant pair",
    observed: `result=${bWriteResult === undefined ? "undefined" : "LINK"} foreignLinkRows=${bForeignLinkRows}`,
    verdict: writeNegPass
      ? verdict.verified(
          "write-path cross-tenant CLOSED: foreign tenant cannot link another tenant's entity (gate refuses, no row written)",
        )
      : verdict.fail(),
    pass: writeNegPass,
    signals: { bWriteRefused: bWriteResult === undefined, bForeignLinkRows },
  });

  // WRITE-POS (pair for Rule 18) — B linking two of its OWN entities succeeds and
  // persists exactly one tenant-B-owned row.
  let bOwnLink: ER.EntityLink | undefined = undefined;
  await runRequestPhaseTx(TENANT_B, async () => {
    bOwnLink = await ER.createLink(
      q(TENANT_B, "ENT-DEV-2"), // B's isolated device — both endpoints B-owned
      q(TENANT_B, "ENT-ACC-1"),
      "SHARED_DEVICE",
      ["rp03 own-tenant write positive"],
      { tenantId: TENANT_B },
    );
  });
  const bOwnLinkRows = bOwnLink
    ? await ownerCount("er_entity_links", "id = $1", [(bOwnLink as ER.EntityLink).id])
    : 0;
  const writePosPass =
    !!bOwnLink &&
    (bOwnLink as ER.EntityLink).id.startsWith(`${TENANT_B}::`) &&
    bOwnLinkRows === 1;
  run.leg({
    id: "WRITE-POS",
    scenario:
      "POSITIVE — tenant B links two of its OWN entities (real service path, aegis_app)",
    expectation:
      "createLink SUCCEEDS (both endpoints tenant-visible) and persists exactly one tenant-B-qualified er_entity_links row",
    observed: `result=${bOwnLink ? "LINK" : "undefined"} ownLinkRows=${bOwnLinkRows}`,
    verdict: writePosPass
      ? verdict.verified(
          "owner-tenant write authenticity: a within-tenant createLink persists exactly its own tenant-qualified link row",
        )
      : verdict.fail(),
    pass: writePosPass,
    signals: { bOwnLinkId: bOwnLink ? (bOwnLink as ER.EntityLink).id : null, bOwnLinkRows },
  });

  // ── PHASE 7 — EMPTY-TENANT FAIL-CLOSED (no tenant / "default") ────────────────
  // The gate (authorizedTenant) short-circuits BEFORE any DB access, so these run
  // outside a tenant tx. Readers return empty; the writer returns undefined and
  // writes nothing.
  const dashDefault = await ER.getEntityResolutionDashboard({ tenantId: "default" });
  const clustersUndef = await ER.detectClusters({ tenantId: undefined });
  const allClustersDefault = await ER.getAllClusters({ tenantId: "default" });
  const writeDefault = await ER.createLink(
    A_ENT,
    q(TENANT_A, "ENT-ACC-2"),
    "TRANSACTION",
    ["rp03 empty-tenant probe"],
    { tenantId: "default" },
  );
  const failClosedPass =
    dashDefault.totalEntities === 0 &&
    dashDefault.totalLinks === 0 &&
    dashDefault.totalClusters === 0 &&
    clustersUndef.length === 0 &&
    allClustersDefault.length === 0 &&
    writeDefault === undefined;
  run.leg({
    id: "FAIL-CLOSED",
    scenario:
      "FAIL-CLOSED — no-tenant / 'default' ctx drives readers + writer (gate short-circuits before DB)",
    expectation:
      "readers return empty (0/[]) and the writer returns undefined with no row written — never a cross-tenant read or an unstamped write",
    observed:
      `dash(ent/links/clusters)=${dashDefault.totalEntities}/${dashDefault.totalLinks}/${dashDefault.totalClusters} ` +
      `detect=${clustersUndef.length} allClusters=${allClustersDefault.length} write=${writeDefault === undefined ? "undefined" : "LINK"}`,
    verdict: failClosedPass
      ? verdict.verified(
          "fail-closed: an unscoped/placeholder tenant yields empty reads and a dropped write — no cross-tenant leak, no unstamped row",
        )
      : verdict.fail(),
    pass: failClosedPass,
    signals: {
      dashEntities: dashDefault.totalEntities,
      detect: clustersUndef.length,
      allClusters: allClustersDefault.length,
      writeRefused: writeDefault === undefined,
    },
  });

  // ── REPORT ───────────────────────────────────────────────────────────────────
  run.report();
  await run.writeJson();

  await cleanup();
  console.log(
    `\ncleanup: deleted all er_entities/er_clusters/er_entity_links rows for ${TENANT_A} + ${TENANT_B}.`,
  );

  const { allPass } = run.acceptance();
  await endOwnerPool();
  await appPool.end();
  if (!allPass) {
    console.error("\n❌ READ-PATH PROOF FAILED — see failing legs above.");
    process.exit(1);
  }
  console.log(
    "\n✅ F-RP-03 read-path + clustering + write cross-tenant CLOSED for all three ER tables (DEV, as aegis_app). HOLD at publish gate.",
  );
}

main().catch(async (err) => {
  console.error("FATAL:", err);
  try {
    await cleanup();
  } catch {}
  try {
    await endOwnerPool();
  } catch {}
  try {
    await appPool.end();
  } catch {}
  process.exit(1);
});
