// scripts/verify-adversarial-retirement.ts
// FU-018 (2026-05-06) — adversarial example harness retirement verification.
// 10 assertions across function-body, audit-log, route, source-static, and DB layers.
// Schema-vocabulary mismatch retirement under bifurcated count semantics
// (FU-011 amendment). Refusal-criterion #5: real DB at every step.
//
// Invocation:  npx tsx scripts/verify-adversarial-retirement.ts
import { db } from "../server/db";
import { adversarialExampleRuns, auditLogs } from "@shared/schema";
import { runAdversarialExamples, listAdversarialExampleRuns } from "../server/lib/wave13-pilot-readiness";
import { desc, eq, sql } from "drizzle-orm";
import { readFileSync } from "fs";
import { join } from "path";

const TRIGGERED_BY = "verify-adversarial-retirement-script";
const ROOT = process.cwd();
let pass = 0, fail = 0;
const out = (label: string, ok: boolean, detail = "") => {
  console.log(`${ok ? "PASS" : "FAIL"}  ${label}${detail ? "  — " + detail : ""}`);
  if (ok) pass++; else { fail++; process.exitCode = 1; }
};

async function main() {
  console.log("─── FU-018 adversarial retirement verification ───");

  // ── Function-body layer (load-bearing diff) ──
  // 1. runAdversarialExamples throws with FU-018 marker
  let threw = false; let msg = "";
  try {
    await runAdversarialExamples({ attackKind: "pgd", samples: 50, modelRef: "kyt-v1", triggeredBy: TRIGGERED_BY });
  } catch (e: any) {
    threw = true; msg = String(e?.message || "");
  }
  out("1. runAdversarialExamples throws with FU-018 marker",
      threw && msg.includes("FU-018 RETIREMENT") && msg.includes("FOLLOW-UPS.md FU-018"),
      threw ? `msg.len=${msg.length}` : "did not throw");

  // 2. audit_logs row written on invocation (function-surface, three-channel pattern)
  // Wait briefly for any pending writes, then query most recent rows.
  await new Promise((r) => setTimeout(r, 250));
  const recentAudits = await db.select().from(auditLogs)
    .where(eq(auditLogs.action, "WAVE13_ADVERSARIAL_RETIREMENT_INVOKED"))
    .orderBy(desc(auditLogs.timestamp))
    .limit(5);
  const fnAudit = recentAudits.find((r) => r.resource === "runAdversarialExamples");
  out("2. audit_logs row written with WAVE13_ADVERSARIAL_RETIREMENT_INVOKED action (function-surface)",
      !!fnAudit,
      fnAudit ? `id=${fnAudit.id} resource=${fnAudit.resource}` : `none of ${recentAudits.length} recent matched resource=runAdversarialExamples`);

  // ── Route layer ──
  // 3. Route 410 with retiredAt + code fields. Check static source rather than HTTP
  // (no auth-bearing test client wired for this script; static check is sufficient
  // since the route handler is a single sync expression with literal values).
  const routesSrc = readFileSync(join(ROOT, "server/routes.ts"), "utf8");
  const routeBlock = routesSrc.slice(routesSrc.indexOf("// 11. Adversarial Examples"), routesSrc.indexOf("/api/wave13/adversarial/list") + 200);
  const has410 = routeBlock.includes("status(410)");
  const hasRetiredAt = routeBlock.includes('retiredAt: "2026-05-06"');
  const hasCode = routeBlock.includes('"WAVE13_ADVERSARIAL_RETIRED"');
  const hasHistorical = routeBlock.includes("retainedReceipts: 35");
  out("3. POST route returns 410 with retiredAt + code + historical fields",
      has410 && hasRetiredAt && hasCode && hasHistorical,
      `410=${has410} retiredAt=${hasRetiredAt} code=${hasCode} historical=${hasHistorical}`);

  // ── DB layer (immutability) ──
  // 4. adversarial_example_runs row count is 35 (frozen, no new writes)
  const [{ n }] = await db.select({ n: sql<number>`count(*)::int` }).from(adversarialExampleRuns);
  out("4. adversarial_example_runs row count = 35 (frozen)",
      Number(n) === 35, `count=${n}`);

  // 10. listAdversarialExampleRuns returns same 35 rows (read path preserved)
  const listed = await listAdversarialExampleRuns(100);
  out("10. listAdversarialExampleRuns returns same 35 rows (read path preserved)",
      Array.isArray(listed) && listed.length === 35,
      `listed=${listed.length}`);

  // ── Static-source layer (touch-point coverage) ──
  const w13Src = readFileSync(join(ROOT, "server/lib/wave13-pilot-readiness.ts"), "utf8");
  const extras8Src = readFileSync(join(ROOT, "server/lib/pilot-readiness-extras-8.ts"), "utf8");
  const uiSrc = readFileSync(join(ROOT, "client/src/pages/wave13-pilot-readiness.tsx"), "utf8");

  // 5. Smoke driver no longer references adversarial as runnable entry
  // (must not contain a `module: "adversarial"` exec entry calling runAdversarialExamples)
  const smokeHasAdvEntry = /\{\s*module:\s*"adversarial"\s*,\s*exec:/.test(w13Src);
  out("5. smoke driver no longer has runnable adversarial entry (14→13 modules)",
      !smokeHasAdvEntry, smokeHasAdvEntry ? "entry still present" : "removed");

  // 6. Monthly-attestation rollup excludes adversarial
  // (must not contain `id: "adversarial"` in the wave13Summary checks array)
  const summaryHasAdv = /id:\s*"adversarial"/.test(w13Src);
  out("6. wave13Summary monthly-attestation rollup excludes adversarial",
      !summaryHasAdv, summaryHasAdv ? "id: \"adversarial\" still present" : "removed");

  // 7. Scoreboard destructure has 23 vars (was 24 with w13Adv)
  // Match the destructure pattern and count comma-separated identifiers.
  const destructureMatch = extras8Src.match(/const\s+\[\s*(canary[^\]]+)\s*\]\s*=\s*await\s+Promise\.all\(/);
  const varCount = destructureMatch
    ? destructureMatch[1].split(",").map((s) => s.trim()).filter((s) => s.length > 0 && !s.startsWith("//")).length
    : -1;
  // Strip line and block comments before checking for w13Adv as a code identifier
  // (the FU-018 banner mentions "w13Adv removed" so a naive grep would match the comment).
  const extras8CodeOnly = extras8Src
    .replace(/\/\*[\s\S]*?\*\//g, "")
    .split("\n")
    .map((l) => l.replace(/\/\/.*$/, ""))
    .join("\n");
  const hasW13Adv = /\bw13Adv\b/.test(extras8CodeOnly);
  out("7. scoreboard destructure has 23 vars (w13Adv removed from code, not just comments)",
      varCount === 23 && !hasW13Adv, `varCount=${varCount} w13AdvInCode=${hasW13Adv}`);

  // 8. Modules list count is 13 wave13-* entries (was 14 with wave13-adversarial)
  const wave13Modules = (extras8Src.match(/module:\s*"wave13-[a-z-]+"/g) || []);
  const hasAdvModule = wave13Modules.some((m) => m.includes("wave13-adversarial"));
  out("8. modules list has 13 wave13-* entries (wave13-adversarial removed)",
      wave13Modules.length === 13 && !hasAdvModule,
      `wave13ModuleCount=${wave13Modules.length} hasAdvModule=${hasAdvModule}`);

  // 9. UI disclosure panel renders the retirement date string + key markers
  const uiHasDate = uiSrc.includes("Retired 2026-05-06");
  const uiHasDisclosure = uiSrc.includes('data-testid="text-adversarial-disclosure"');
  const uiHasHistContainer = uiSrc.includes('data-testid="container-adversarial-historical"');
  const uiHasHideRunBtnProp = /hideRunButton\?:\s*boolean/.test(uiSrc);
  out("9. UI disclosure panel renders date + disclosure + historical container + hideRunButton prop",
      uiHasDate && uiHasDisclosure && uiHasHistContainer && uiHasHideRunBtnProp,
      `date=${uiHasDate} disclosure=${uiHasDisclosure} histContainer=${uiHasHistContainer} hideRunBtnProp=${uiHasHideRunBtnProp}`);

  // 11. No stale "14 modules" / "14 Wave-13 modules" text in Wave-13 user-facing surfaces.
  // Architect-flagged audit-facing consistency requirement (FU-018 review 2026-05-06).
  // Permitted exceptions: comments that explicitly document the 14→13 retirement chronology
  // (i.e. "Was 14" or "(was 14" parentheticals).
  const stripChronology = (s: string) =>
    s.split("\n").filter((l) => !/Was\s+14|\(was\s+14/i.test(l)).join("\n");
  const w13Stripped = stripChronology(w13Src);
  const extras8Stripped = stripChronology(extras8Src);
  const uiStripped = stripChronology(uiSrc);
  const stale14 = [
    ...Array.from(w13Stripped.matchAll(/14\s+(active\s+)?(Wave-13\s+)?modules/gi)).map((m) => `w13:${m[0]}`),
    ...Array.from(extras8Stripped.matchAll(/Wave-13[^\n]*\(14\s+modules\)/gi)).map((m) => `extras8:${m[0]}`),
    ...Array.from(uiStripped.matchAll(/14\s+(Wave-13\s+)?modules|Running\s+14\s+modules/gi)).map((m) => `ui:${m[0]}`),
  ];
  out("11. no stale '14 modules' text remains in Wave-13 surfaces (audit consistency, FU-018 review)",
      stale14.length === 0,
      stale14.length === 0 ? "all updated to 13" : `still ${stale14.length} stale ref(s): ${stale14.join(" | ")}`);

  console.log(`\n─── ${pass}/${pass + fail} assertions passed ───`);
  if (fail > 0) console.log(`${fail} FAILED — see PASS/FAIL log above`);
  process.exit(fail > 0 ? 1 : 0);
}

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