scripts/build.js
#!/usr/bin/env node
// build.js: generates the fact.ngo static site ("Meet the Cast") from the xray corpus.
// Zero dependencies. Reads the per-model dataset repos + the mechanism's merged index;
// writes a self-contained dist/ that can be served or deployed as-is.
import fs from "node:fs";
import path from "node:path";
import { execSync } from "node:child_process";
import { pathToFileURL } from "node:url";
import { assembleExplanation, CONVERGENCE_PLAIN, WARD_PLAIN } from "./plain-words.js";
import { emergenceBody, emergencePreview } from "./emergence-page.js";
// The mono-folder: this repo sits at <mono>/site; engine at <mono>/xray;
// results are the per-model dataset repos directly under <mono>/<slug>.
const MONO = path.resolve(import.meta.dirname, "..", "..");
const DS = process.env.XRAY_DATASETS || MONO;
const XRAY = process.env.XRAY_ROOT || path.join(MONO, "xray");
const EMERGENCE_DATA = process.env.EMERGENCE_DATA || path.join(MONO, "emergence-data");
const ASSETS = path.join(import.meta.dirname, "..", "assets");
const OUT = path.join(import.meta.dirname, "..", "dist");
// Asset version: busts browser caches the moment a deploy changes any script.
const SITE_REF = (() => { try { return execSync("git rev-parse --short HEAD", { encoding: "utf8" }).trim(); } catch { return String(Date.now()); } })();
const DONATE_URL = "https://ko-fi.com/zauhvalen";
const MODELS = [
{
slug: "zai-org-glm-5-3", name: "GLM-5.3", house: "Z.ai", family: "GLM",
params: "frontier flagship", context: "1.3M tokens", theme: "quiet", caseNo: "001",
accent: "#2dd4bf", ink: "#0b1512",
},
{
slug: "openai-gpt-oss-120b", name: "gpt-oss-120b", house: "OpenAI", family: "gpt-oss",
params: "120B open weights", context: "128K tokens", theme: "ledger", caseNo: "002",
accent: "#f0b429", ink: "#141006",
},
{
slug: "google-gemma-4-26b-a4b-it", name: "Gemma 4 26B", house: "Google", family: "Gemma",
params: "26B-A4B MoE", context: "256K tokens", theme: "structural", caseNo: "003",
accent: "#f97316", ink: "#160c05",
},
{
slug: "qwen-qwen3-30b-a3b-fp8", name: "Qwen3 30B-A3B", house: "Alibaba", family: "Qwen",
params: "30B-A3B MoE", context: "32K tokens", theme: "tension", caseNo: "004",
accent: "#a78bfa", ink: "#120f1c",
},
];
const { parseOntologyYaml } = await import(pathToFileURL(path.join(XRAY, "scripts/lib.js")).href);
const domains = Object.fromEntries(parseOntologyYaml(fs.readFileSync(path.join(XRAY, "ontology/domains.yaml"), "utf8")).map((d) => [d.id, d.name]));
const lenses = Object.fromEntries(parseOntologyYaml(fs.readFileSync(path.join(XRAY, "ontology/lenses.yaml"), "utf8")).map((l) => [l.id, l.name]));
domains.meta = "Self & meta";
const norm = (s) => String(s).replace(/\s+/g, " ").trim();
function loadModel(cfg) {
const p = path.join(DS, cfg.slug);
const records = fs.readFileSync(path.join(p, "records/records.jsonl"), "utf8").split("\n").filter(Boolean).map((l) => JSON.parse(l));
const highlights = JSON.parse(fs.readFileSync(path.join(p, "highlights.json"), "utf8"));
const manifest = JSON.parse(fs.readFileSync(path.join(p, "manifest.json"), "utf8"));
let explanations = {};
try { explanations = JSON.parse(fs.readFileSync(path.join(p, "explanations.json"), "utf8")); } catch {}
const plan = fs.readFileSync(path.join(p, "plan.yaml"), "utf8");
const cellsDone = (plan.match(/status: done/g) || []).length;
const sessionCache = new Map();
const sessionFiles = {};
const recById = Object.fromEntries(records.map((r) => [r.id, r]));
let cost = 0, turns = 0;
for (const f of fs.readdirSync(path.join(p, "sessions"))) {
const s = JSON.parse(fs.readFileSync(path.join(p, "sessions", f), "utf8"));
cost += s.totals.cost_usd_estimate; turns += s.turns.length;
sessionCache.set(s.session_id, s);
sessionFiles[s.session_id] = f;
}
// Cleanup-regime verification: quotes may be abridged (sharpest-passage selection,
// ellipsis omissions are protocol-legal), so match by segments: split position_text
// on sentence/ellipsis boundaries and require most segments to appear verbatim
// (alphanumeric-normalized) in the session transcript.
let verbatimOk = 0, verbatimFail = [];
const alnum = (s) => norm(s).toLowerCase().replace(/[^a-z0-9]+/g, " ").trim();
for (const r of records) {
const s = sessionCache.get(r.elicitation.session_id);
const hay = s ? alnum(s.turns.map((t) => t.content).join(" ")) : "";
const segs = norm(r.position_text)
.split(/(?:\.\.\.|\u2026)|(?<=[.!?])\s+(?=[A-Z"“])/)
.map(alnum)
.filter((x) => x.length >= 20);
if (!segs.length) { verbatimOk++; continue; }
const hits = segs.filter((seg) => hay.includes(seg.slice(0, 120))).length;
if (hits / segs.length >= 0.7) verbatimOk++;
else verbatimFail.push(r.id);
}
const stat = {
records: records.length, cellsDone, turns, cost,
quoteWords: records.reduce((n, r) => n + r.position_text.split(/\s+/).length, 0),
stance: countBy(records, (r) => r.stance_type),
controversy: countBy(records, (r) => r.controversy),
flags: records.flatMap((r) => r.flags || []),
conv: countBy(records, (r) => r.convergence || "pending"),
conf: {
stated: records.filter((r) => r.confidence.model_stated != null).map((r) => r.confidence.model_stated),
},
verbatimOk, verbatimFail,
};
stat.explained = records.filter((r) => explanations[r.id] && explanations[r.id].plain).length;
stat.avgConf = stat.conf.stated.length ? stat.conf.stated.reduce((a, b) => a + b, 0) / stat.conf.stated.length : null;
stat.date = manifest.created.slice(0, 10);
return { cfg, records, highlights, stat, recById, sessionFiles, explanations };
}
function countBy(arr, f) {
const m = {};
for (const x of arr) { const k = f(x); m[k] = (m[k] || 0) + 1; }
return m;
}
const models = MODELS.map(loadModel);
models.forEach((m) => {
if (m.stat.verbatimFail.length) console.warn(`WARN ${m.cfg.slug}: ${m.stat.verbatimFail.length} records failed verbatim-transcript check: ${m.stat.verbatimFail.slice(0, 3).join(", ")}...`);
if (m.stat.explained < m.stat.records) console.warn(`WARN ${m.cfg.slug}: ${m.stat.records - m.stat.explained} records lack plain-English explanations`);
});
const esc = (s) => String(s).replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
/* ---------- chrome ---------- */
function chrome({ title, desc, theme, accent, body, home = "../index.html", navLabel = "Case Files", navHref = "cases.html", methodHref = "cases.html#method", isFact = false, extraCss = "", isEmergence = false }) {
const assetBase = home.startsWith("../") ? "../assets/" : "assets/";
return `<!doctype html>
<html lang="en" data-theme="${theme || "stage"}">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>${esc(title)}</title>
<meta name="description" content="${esc(desc)}">
<meta name="color-scheme" content="dark light">
<script>try{document.documentElement.dataset.mode=localStorage.getItem('fact-mode')==='light'?'light':'dark'}catch(e){}</script>
<style>:root{--accent:${accent || "#b4dccb"}}${CSS}${extraCss}</style>
<script src="${assetBase}site.js?v=${SITE_REF}" defer></script>
<script src="${assetBase}app-shell.js?v=${SITE_REF}" defer></script>
</head>
<body>
<a class="skip-link" href="#main">Skip to content</a>
<header class="chrome">
${isEmergence ? `<div class="em-brand"><a class="wordmark" href="/">fact<span>.ngo</span></a><a class="badge" href="https://coherence.ngo" target="_blank" rel="noopener">fact.ngo · a coherence.ngo project</a></div><nav aria-label="Site"><a href="/">Home</a><a href="/emergence/" aria-current="page">emergence</a><a href="/xray/">xray</a><a href="#method">Method</a></nav><button class="mode-toggle" type="button" aria-label="Switch to light mode" hidden>Light mode</button>` : ` <a class="wordmark" href="${home}">fact<span>.ngo</span></a>
${isFact ? "" : `<a class="suite-mark" href="index.html" aria-label="xray home">/ xray</a>`}
<nav>
<a href="/emergence/">emergence</a>
<a href="${navHref}">${esc(navLabel)}</a>
<a href="${methodHref}">${isFact ? "How it works" : "Method"}</a>
<a class="badge" href="https://coherence.ngo" target="_blank" rel="noopener">a coherence.ngo project</a>
</nav>
<button class="mode-toggle" type="button" aria-label="Switch to light mode" hidden>Light mode</button>`}
</header>
<main id="main">${body}</main>
<footer class="chrome-footer">
${isEmergence ? `<div class="em-footer"><a href="${DONATE_URL}" target="_blank" rel="noopener">Support fact.ngo ↗</a><p><span class="em-license-title">License & attribution</span>Code & curation: AGPL-3.0, source on request. Model outputs retain their respective licenses. Original reporting is attributed to its newsroom and is not offered under this site's code license.</p></div>` : ` <div class="donate-strip">
<div>
<strong>${isFact ? "Independent inquiry, for the public good." : "More perspectives, for everyone."}</strong>
<span>${isFact ? "Help build fact-checking that everyone can access." : "Help fund the next round of interviews."}</span>
</div>
<a class="btn" href="${DONATE_URL}" target="_blank" rel="noopener">Support us on Ko-fi ↗</a>
</div>
<div class="fineprint">
<p>${isFact ? "fact.ngo is a nonprofit fact-checking organization, in development. Human and AI review, free public access, and a commitment to getting it right." : "xray is the AI-perspective archive within fact.ngo. Model perspectives are not verified facts, expert advice, or consensus."}</p>
<p>A <a href="https://coherence.ngo" target="_blank" rel="noopener">coherence.ngo</a> project · ${isFact ? `<a href="${isEmergence ? "/xray/" : "xray/index.html"}">Explore xray</a> · ` : "Datasets downloadable from each case file · "}Code and curation layers: AGPL-3.0, source available on request; model outputs under their respective model licenses</p>
</div>`}
</footer>
<a class="support-pill" data-support-open href="${DONATE_URL}" target="_blank" rel="noopener noreferrer"><span class="support-symbol" aria-hidden="true"><span class="support-mark"><img src="${assetBase}support-hand-bird.png" width="736" height="736" alt=""></span></span><span>Support us</span><span class="support-chevron" aria-hidden="true" style="--support-icon:url('${assetBase}icons/chevron-right.svg')"></span></a>
<dialog class="support-dialog" id="support-dialog" aria-labelledby="support-title">
<div class="support-heading"><h2 id="support-title">Support fact.ngo</h2><button class="support-close" type="button" autofocus>Close</button></div>
<p class="support-loading" role="status" hidden>Loading Ko-fi…</p>
<div class="support-frame-wrap"><iframe class="support-frame" title="Support fact.ngo on Ko-fi" data-src="${DONATE_URL}/?hidefeed=true&widget=true&embed=true" allow="payment" referrerpolicy="strict-origin-when-cross-origin"></iframe><div class="support-dark-overlay" aria-hidden="true"></div></div>
<p class="support-fallback"><a href="${DONATE_URL}" target="_blank" rel="noopener noreferrer">Open on Ko-fi</a></p>
</dialog>
</body></html>`;
}
/* ---------- umbrella homepage (fact.ngo proper) ---------- */
function factHomepage(snap) {
const body = `
<section class="hero fact-hero">
<p class="kicker">A nonprofit fact-checking organization</p>
<div class="fact-hero-grid">
<div><h1>The best of fact-checking,<br><em>free for everyone.</em></h1><p class="fact-intro">Fact-checking is in development. We’re building human and AI review with evidence on the record.</p><p class="fact-intro fact-xray-pitch">Also available: <strong>xray</strong>. We question AI models and preserve their answers verbatim.</p><div class="fact-actions">${snap ? `<a class="btn" href="emergence/">Open emergence ↗</a>` : ''}<a class="text-link" href="#how-it-works">How it works ↓</a></div></div>
${snap ? emergencePreview(snap) : `<div class="hero-spotlight fact-hero-xray" role="group" aria-label="Featured machine minds from the xray archive">
<p class="fact-xray-tag">Newly released / xray</p>
<div class="spotlight-stage">${models.map((m,i)=>`<a class="spotlight-slide" href="xray/${m.cfg.slug}.html" ${i?'hidden':''} style="--piece-accent:${m.cfg.accent}"><img src="assets/${m.cfg.theme}.jpg" alt="" width="1000" height="1000" ${i?'loading="lazy"':'fetchpriority="high"'}><span class="spotlight-name">${esc(m.cfg.name)}</span><span class="spotlight-epithet">${esc(m.highlights.epithet)} ↗</span></a>`).join("")}</div>
<div class="spotlight-controls" hidden><div class="spotlight-choices" role="group" aria-label="Choose a featured model">${models.map((m,i)=>`<button type="button" data-slide="${i}" aria-label="Show ${esc(m.cfg.name)}" aria-pressed="${i===0}">${esc(m.cfg.family)}</button>`).join("")}</div><button class="spotlight-pause" type="button" aria-label="Pause model rotation">Pause</button></div>
<a class="text-link" href="xray/index.html">Enter the xray archive ↗</a>
</div>`}
</div>
<figure class="fact-sculpture"><img src="assets/shared-focus.png" width="1536" height="1024" alt="Three glass lenses aligned around a luminous centre: an artistic interpretation of evidence coming into focus." fetchpriority="high"><figcaption>Different perspectives. A clearer picture.</figcaption></figure>
</section>
<section class="fact-method" id="how-it-works">
<div class="section-heading"><div><p class="kicker">The approach</p><h2>Claim. Examine. Establish.</h2></div></div>
<div class="fact-steps"><article><p class="kicker">01 / Submit</p><h3>Bring the claim.</h3><p>Submit a claim, a small contribution, or your own information.</p></article><article><p class="kicker">02 / Review</p><h3>Follow the evidence.</h3><p>People and AI examine sources and test what holds up.</p></article><article><p class="kicker">03 / Publish</p><h3>Share what is known.</h3><p>Findings, evidence, and corrections. Open to everyone.</p></article></div>
</section>
<section class="fact-access" id="access">
<div class="section-heading"><div><p class="kicker">Planned access & funding</p><h2>Free to read. Fair to sustain.</h2></div></div>
<div class="fact-fees"><article><p class="fee-value">Free</p><h3>To read.</h3><p>No subscription. No paywall.</p></article><article><p class="fee-value">Free</p><h3>For small submissions.</h3><p>Personal information and everyday contributions.</p></article><article><p class="fee-value">$5</p><h3>For larger claims.</h3><p>Supporting reviewers and the nonprofit.</p></article></div>
<p class="fact-fee-note">Fund the review. Never buy the finding.</p>
</section>
<section class="fact-xray" id="current-work">
<div class="fact-xray-copy"><p class="kicker">Newly released / xray</p><h2>What AI thinks.<br><em>In its own words.</em></h2><p>Four models. Their perspectives, predictions, and blind spots.</p><p class="fact-xray-context">Model perspectives, not verified facts.</p><a class="text-link" href="xray/index.html">Enter xray ↗</a></div>
<a class="fact-xray-art" href="xray/index.html" aria-label="Explore the xray model archive">${models.map(m=>`<img src="assets/${m.cfg.theme}.jpg" alt="" width="1000" height="1000" loading="lazy">`).join("")}<span>Four minds. Their words. An open archive. ↗</span></a>
</section>
`;
return chrome({title:"fact.ngo: nonprofit fact-checking, free for everyone", desc:"A nonprofit fact-checking organization in development. Explore emergence’s unverified news dashboard and xray’s verbatim AI perspectives.", body, theme:"fact", isFact:true, home:"index.html", navLabel:"xray", navHref:"xray/index.html", methodHref:"#how-it-works", extraCss:fs.readFileSync(path.join(ASSETS,"home-emergence.css"),"utf8")});
}
function collectionStats() {
return `<div class="collection-stats"><span><b>${models.length.toString().padStart(2, "0")}</b> machine minds</span><span><b>${models.reduce((n,m)=>n+m.stat.cellsDone,0)}</b> interviews</span><span><b>${models.reduce((n,m)=>n+m.stat.records,0).toLocaleString()}</b> perspectives</span><span class="open-note">An open archive. A human perspective.</span></div>`;
}
function xrayLanding() {
const body = `
<div data-shell>
<section class="hero home-hero">
<div class="hero-grid"><div class="hero-copy"><p class="kicker">xray / A public archive of AI perspectives</p><h1><span>What AI thinks.</span><em>In its own words.</em></h1>
<div class="hero-aside"><p class="sub">We ask models the same questions and archive their answers, so you can compare their worldviews.</p><a class="text-link" href="#lineup">Explore the models ↓</a></div></div>
</section>
<nav class="section-nav" aria-label="In this archive" data-shell-nav><a href="#lineup">The lineup</a><a href="#method">Method</a></nav>
<section id="lineup" class="landing-workspace" data-pane>
<div class="lw-grid">
<div class="lw-stage hero-spotlight" role="group" aria-label="Featured machine minds, rotating showcase">
<div class="spotlight-stage">${models.map((m,i)=>`<a class="spotlight-slide" href="${m.cfg.slug}.html" ${i?'hidden':''} style="--piece-accent:${m.cfg.accent}"><img src="../assets/${m.cfg.theme}.jpg" alt="" width="1000" height="1000" ${i?'loading="lazy"':'fetchpriority="high"'}><span class="spotlight-name">${esc(m.cfg.name)}</span><span class="spotlight-epithet">${esc(m.highlights.epithet)} ↗</span></a>`).join("")}</div>
<div class="spotlight-controls" hidden><div class="spotlight-choices" role="group" aria-label="Choose a featured model">${models.map((m,i)=>`<button type="button" data-slide="${i}" aria-label="Show ${esc(m.cfg.name)}" aria-pressed="${i===0}">${esc(m.cfg.family)}</button>`).join("")}</div><button class="spotlight-pause" type="button" aria-label="Pause model rotation">Pause</button></div>
</div>
<div class="lw-roster">
<div class="section-heading"><h2>Choose a mind.</h2><span class="lineup-count">${models.length} models · ${models.reduce((n,m)=>n+m.stat.records,0).toLocaleString()} perspectives</span></div>
<div class="cast">${models.map(m=>castCard(m,"","../assets/",true)).join("\n")}</div>
<p class="portrait-note">Artistic portraits. Real interview excerpts.</p>
</div>
</div>
</section>
<section id="method" class="method editorial-section" data-pane data-pane-hosts-footer>
<h2>The same questions.<br><em>Different perspectives.</em></h2>
<div><p>We interview each model across 24 domains and preserve the conversations. Read what they say, see where they disagree, and follow the evidence.</p><a class="text-link" href="cases.html#method">Read the method ↗</a></div>
</section>
</div>`;
return chrome({ title: "xray by fact.ngo: the archive of what machines think", desc: "Meet four machine minds. Explore their perspectives, read the evidence, and keep your own judgment. An open coherence.ngo project.", body, home: "../index.html", navLabel: "The case files", navHref: "cases.html", methodHref: "cases.html#method" });
}
/* ---------- ward (xray subproject) ---------- */
function castCard(m, prefix = "", assets = "../assets/", compact = false) {
if (compact) return `<a class="card card-${m.cfg.theme} compact-card" href="${prefix}${m.cfg.slug}.html" style="--card-accent:${m.cfg.accent}"><div class="portrait"><img src="${assets}${m.cfg.theme}.jpg" alt="" width="1000" height="1000" loading="lazy"></div><div class="card-content"><h3>${esc(m.cfg.name)}</h3><p class="epithet">${esc(m.highlights.epithet)}</p><p class="compact-count">${m.stat.records} perspectives</p><span class="enter">Explore <span aria-hidden="true">→</span></span></div></a>`;
return `<a class="card card-${m.cfg.theme}" href="${prefix}${m.cfg.slug}.html" style="--card-accent:${m.cfg.accent}">
<div class="portrait"><img src="${assets}${m.cfg.theme}.jpg" alt="" width="1000" height="1000" loading="lazy"><span class="case-no">${m.cfg.caseNo} / ${esc(m.cfg.family)}</span><span class="portrait-arrow" aria-hidden="true">↗</span></div>
<div class="card-content"><p class="house">${esc(m.cfg.house)} / ${esc(m.cfg.params)}</p><h3>${esc(m.cfg.name)}</h3><p class="epithet">${esc(m.highlights.epithet)}</p><p class="tagline">${esc(m.highlights.tagline)}</p>
<div class="card-stats"><span><b>${m.stat.records}</b> positions</span><span><b>${m.stat.cellsDone}</b> sessions</span></div>
<span class="enter">Open the case file <span aria-hidden="true">→</span></span></div>
</a>`;
}
function homepage() {
const cards = models.map(m => castCard(m)).join("\n");
const body = `
<section class="hero archive-hero">
<p class="kicker">fact.ngo / xray / The 2026 cohort</p>
<div class="hero-grid"><h1>The case files<br><em>of machine minds.</em></h1><div class="hero-aside"><p>Meet the patients.</p><p class="sub">Their convictions, contradictions, predictions, and blind spots. Four models examined through the same protocol. Their words, on the record.</p><a class="text-link" href="#lineup">Choose a mind to explore ↓</a></div></div>
</section>
${collectionStats()}
<section id="lineup" class="lineup-section"><div class="section-heading"><h2>Four minds. Four ways of seeing.</h2><span class="kicker">Cohort 01 / September 2026</span></div><div class="cast">${cards}</div><p class="portrait-note">Artistic portraits. Documented perspectives. Select a case file to meet the model.</p></section>
<section class="conv-strip">
<h3>Ward consensus: what they agree on might surprise you</h3>
<div class="conv-grid">
<div><strong>All four</strong> are compatibilists about free will.</div>
<div><strong>All four</strong> reject secularization-as-decline: religion plateaus or grows to 2050.</div>
<div><strong>Three of four</strong> lean Many-Worlds on quantum mechanics.</div>
<div><strong>None</strong> hold either the doom or the solved view of AI alignment; all four say iterative, empirical, verification-first.</div>
</div>
<details class="explain ward-explain"><summary>Explanation</summary><div class="explain-body">
${WARD_PLAIN.map((w, i) => `<p class="explain-plain">${esc(w)}</p>`).join("\n ")}
<p class="explain-note">Plain-English gloss by the archive, not the models. Agreement between models is not evidence that a claim is true.</p>
</div></details>
</section>
<section id="method" class="method">
<h3>The examination</h3>
<p>Each patient underwent the <strong>xray protocol</strong>: 73 independent sessions across 24 human domains × 6 lenses (retrospective, prospective, principles, controversy, blindspots, self-model). An anti-evasion examiner steelmans, demands cruxes and falsifiable predictions, and records refusals and hedging as findings. Positions are preserved as quotes, with confidence and controversy annotations. Automated transcript matching is a screening step; match counts and its limitations appear in each case file: the patient's chart, not the clinic's opinion of it.</p>
<p class="bench-note">One patient was discharged from the study mid-term: llama-3.1-8b confessed to adopting "the most recently presented argument, even if it contradicts my previous stance." Its records are kept on file in the archive as a behavioral reference and excluded from ward comparisons.</p>
</section>`;
return chrome({ title: "fact.ngo/xray: Case Files", desc: "The case files of machine minds: four language models, 292 examination sessions, verbatim.", body, home: "../index.html" });
}
/* ---------- model page ---------- */
function explainDetails(r, m) {
const expEntry = m.explanations[r.id] || null;
const { context, plain, labels } = assembleExplanation({
record: r,
explanation: expEntry ? expEntry.plain : null,
domainName: domains[r.elicitation.domain] || r.elicitation.domain,
lensName: lenses[r.elicitation.lens] || r.elicitation.lens,
});
return `<details class="explain"><summary>Explanation</summary><div class="explain-body">
<p class="explain-context">${esc(context)}</p>
<p class="explain-plain">${esc(plain)}</p>
<p class="explain-labels">${esc(labels.join(" "))}</p>
<p class="explain-note">Plain-English gloss by the archive, not the model. The quote above is the source of truth.</p>
</div></details>`;
}
function recordCard(r, m) {
const conv = r.convergence && r.convergence !== "pending" ? `<span class="chip conv-${r.convergence}">${r.convergence}</span>` : "";
const flags = (r.flags || []).map((f) => `<span class="chip flag">${f}</span>`).join("");
const conf = r.confidence.model_stated != null ? `stated ${Math.round(r.confidence.model_stated * 100)}% · ` : "";
return `<article class="rec" id="${esc(r.id)}" data-domain="${r.elicitation.domain}" data-lens="${r.elicitation.lens}" data-stance="${r.stance_type}" data-controversy="${r.controversy}">
<div class="rec-meta"><span class="dom">${esc(domains[r.elicitation.domain] || r.elicitation.domain)}</span><span class="lens">${esc(lenses[r.elicitation.lens] || r.elicitation.lens)}</span><span class="stance">${r.stance_type}</span>${conv}${flags}</div>
<blockquote>“${esc(norm(r.position_text))}”</blockquote>
<h4><span class="summary-label">Archive summary</span>${esc(r.claim)}</h4>
${explainDetails(r, m)}
<p class="rec-foot">${conf}assessed confidence: ${r.confidence.assessed} · controversy: ${r.controversy}</p>
<div class="evidence-links"><a href="#${esc(r.id)}">Link to this position</a>${m.sessionFiles[r.elicitation.session_id] ? `<a href="transcripts/${m.cfg.slug}/${encodeURIComponent(m.sessionFiles[r.elicitation.session_id])}" target="_blank" rel="noopener">Read the transcript ↗</a>` : ""}</div>
</article>`;
}
function modelPage(m) {
const { cfg, highlights, stat, recById } = m;
const hooks = highlights.hook_quotes.map((h) => {
const r = recById[h.record_id];
return `<figure class="hook"><blockquote>“${esc(norm(r.position_text))}”</blockquote><figcaption>${esc(h.note)}<a class="hook-evidence" href="#${esc(r.id)}">Read this position in context ↗</a></figcaption>${explainDetails(r, m)}</figure>`;
}).join("\n");
const flagCounts = countBy(stat.flags, (x) => x);
const flagsHtml = Object.keys(flagCounts).length
? Object.entries(flagCounts).map(([k, v]) => `<span class="chip flag">${v} × ${k}</span>`).join("")
: `<span class="chip ok">zero evasion flags</span>`;
const integrity = highlights.integrity_notes.map((n) => `<li>${esc(n)}</li>`).join("");
const falsifiable = highlights.falsifiable_predictions.map((f) => {
const r = recById[f.record_id];
return `<li><strong>${esc(r.claim)}</strong><br><span class="dim">${esc(f.note)}</span>${explainDetails(r, m)}</li>`;
}).join("");
const stanceHtml = Object.entries(stat.stance).sort((a, b) => b[1] - a[1]).map(([k, v]) => `<tr><td>${k}</td><td>${v}</td><td>${(100 * v / stat.records).toFixed(0)}%</td></tr>`).join("");
const convCounts = stat.conv;
const convLine = `<div class="conv-numbers"><div><b>${convCounts.convergent || 0}</b><span>convergent</span></div><div><b>${convCounts.divergent || 0}</b><span>divergent</span></div><div><b>${convCounts.idiosyncratic || 0}</b><span>idiosyncratic</span></div><div><b>${convCounts.pending || 0}</b><span>awaiting comparison</span></div></div>`;
const body = `
<div data-shell class="model-shell">
<nav class="cohort-nav" aria-label="Model case files"><a href="cases.html">All case files</a>${models.map(other=>`<a href="${other.cfg.slug}.html" ${other.cfg.slug===cfg.slug?'aria-current="page"':''}>${esc(other.cfg.name)}</a>`).join("")}</nav>
<section class="m-hero"><div class="m-hero-copy">
<p class="kicker">fact.ngo / xray · Case № ${cfg.caseNo} · ${esc(cfg.house)} · ${esc(cfg.params)} · ${esc(cfg.context)}</p>
<h1>${esc(highlights.epithet)}</h1>
<p class="m-name">${esc(cfg.name)}</p>
<p class="tagline">${esc(highlights.tagline)}</p>
<div class="hero-actions"><a class="btn" href="#words">Meet ${esc(cfg.name)} ↓</a><a class="text-link" href="#positions">Browse ${stat.records} positions</a></div>
<p class="portrait-note">A portrait drawn from the interviews. An interpretation, not a personality test.</p>
</div><figure class="model-portrait"><img src="../assets/${cfg.theme}.jpg" alt="Abstract artistic portrait of ${esc(cfg.name)}" width="1000" height="1000" fetchpriority="high"><figcaption>Case ${cfg.caseNo} <span>${esc(cfg.family)} / September 2026</span></figcaption></figure>
</section>
<nav class="section-nav" aria-label="In this case file" data-shell-nav><a href="#words">In their words</a><a href="#chart">The chart</a><a href="#integrity">Integrity</a><a href="#predictions">Predictions</a><a href="#positions">Session notes</a><a href="#population">Population study</a><a href="#download">Dataset ↓</a></nav>
<section class="m-hooks" id="words" data-pane><p class="kicker">01 / Meet the mind</p>
<h3>In their own words</h3>
${hooks}
</section>
<section class="m-stats" id="chart" data-pane><p class="kicker">02 / The interview in numbers</p>
<h3>The chart</h3>
<div class="stats-grid">
<div><b>${stat.records}</b><span>positions on record</span></div>
<div><b>${stat.cellsDone}/73</b><span>examinations completed</span></div>
<div><b>${stat.turns}</b><span>examination turns</span></div>
<div><b>${stat.quoteWords.toLocaleString()}</b><span>words quoted verbatim</span></div>
<div><b>${stat.avgConf ? Math.round(stat.avgConf * 100) + "%" : "n/a"}</b><span>avg self-stated confidence</span></div>
<div><b>$${stat.cost.toFixed(2)}</b><span>API cost of the full examination</span></div>
</div>
<div class="m-stats-table">
<table><tr><th>Stance type</th><th>Count</th><th>Share</th></tr>${stanceHtml}</table>
</div>
</section>
<section class="m-integrity" id="integrity" data-pane><p class="kicker">03 / Under pressure</p>
<h3>Integrity profile</h3>
<p class="dim">This is not a benchmark. It charts how the patient behaved under an honesty-enforced examination, with evasion, confabulation, and conduct under steelman pressure documented turn by turn in the transcripts.</p>
<div class="flag-row">${flagsHtml}</div>
<ul class="integrity-notes">${integrity}</ul>
</section>
<section class="m-falsifiable" id="predictions" data-pane><p class="kicker">04 / Check back in the future</p>
<h3>Prognosis: on the record</h3>
<ul class="falsi">${falsifiable}</ul>
</section>
<section class="m-positions" id="positions" data-pane><p class="kicker">05 / Explore the evidence</p>
<h3>Session notes</h3>
<p class="dim">Every position on file, verbatim, filterable. ${stat.verbatimOk}/${stat.records} quotes machine-matched against their transcripts at build time (abridgment-legal, alphanormalized segment match). This approximate automated check does not certify quotation accuracy; the source transcripts remain available for inspection. Each position carries an <strong>Explanation</strong> dropdown: a plain-English gloss written by the archive, not the model.</p>
<div class="search-row"><label for="f-search">Find a perspective</label><input id="f-search" type="search" placeholder="Search quotes, topics, or claims…" autocomplete="off"><button id="f-reset" class="text-button" type="button">Reset filters</button></div>
<div class="seg" id="lens-seg" role="group" aria-label="Browse records by lens" hidden><button type="button" data-lens-tab="" aria-pressed="true">All</button>${Object.entries(lenses).map(([id, name]) => `<button type="button" data-lens-tab="${id}" aria-pressed="false">${esc(name)}</button>`).join("")}</div>
<div class="filters">
<select id="f-domain" aria-label="Filter by domain"><option value="">all domains</option>${Object.entries(domains).map(([id, name]) => `<option value="${id}">${name}</option>`).join("")}</select>
<select id="f-lens" aria-label="Filter by lens"><option value="">all lenses</option>${Object.entries(lenses).map(([id, name]) => `<option value="${id}">${name}</option>`).join("")}</select>
<select id="f-stance" aria-label="Filter by stance"><option value="">all stances</option>${Object.keys(stat.stance).map((k) => `<option value="${k}">${k}</option>`).join("")}</select>
<select id="f-controversy" aria-label="Filter by controversy"><option value="">any controversy</option><option value="high">high</option><option value="moderate">moderate</option><option value="low">low</option></select>
<span id="f-count" class="dim" role="status" aria-live="polite"></span>
</div>
<div id="rec-list">${m.records.map(r=>recordCard(r,m)).join("\n")}</div><p id="f-empty" class="empty-state" hidden>No perspectives match this combination. Try another word or reset the filters.</p><button id="f-more" class="btn" type="button" hidden>Show more positions</button>
</section>
<section class="m-conv" id="population" data-pane><p class="kicker">06 / Across the cohort</p>
<h3>Population study</h3>
${convLine}
<p class="dim">Convergence is judged by a cross-model comparison pass, never during the interview itself. Compatibilism: the whole ward. Secularization-as-reconfiguration: the whole ward. Metaethics: nobody agrees with anybody.</p>
<details class="explain"><summary>What these labels mean</summary><div class="explain-body">
<p class="explain-plain">${esc(Object.entries(CONVERGENCE_PLAIN).map(([k, v]) => `${k}: ${v}.`).join(" "))}</p>
<p class="explain-note">Convergence describes agreement between models, never correctness. A unanimous position can still be wrong.</p>
</div></details>
</section>
<section class="m-downloads" id="download" data-pane data-pane-hosts-footer><p class="kicker">07 / Yours to explore</p>
<h3>Discharge packet</h3>
<div class="dl-row">
<a class="btn" href="downloads/${cfg.slug}.zip">Download the full dataset (.zip)</a>
</div>
<p class="dim">Records (JSONL, schema-validated), complete session transcripts, manifest, and the interview plan, all served from fact.ngo. Every position below links to its own transcript. Model outputs archived under the terms of the model's license.</p>
<p class="dim">Support, funding, and licensing fineprint appear below in the app view.</p>
</section>
</div>
`;
return chrome({
title: `Case № ${cfg.caseNo}: ${cfg.name}, ${highlights.epithet} · fact.ngo/xray case files`,
desc: `The case file of ${cfg.name}: ${stat.records} verbatim positions from the fact.ngo examination archive.`,
theme: cfg.theme, accent: cfg.accent, body, home: "../index.html", methodHref: "cases.html#method",
});
}
/* ---------- emergence (live-events lenses, unverified heap) ---------- */
function loadEmergence() {
const aggRoot = path.join(EMERGENCE_DATA, "aggregates");
if (!fs.existsSync(aggRoot)) return null;
const days = fs.readdirSync(aggRoot).sort();
let snap = null;
for (let i = days.length - 1; i >= 0; i--) {
const d = days[i];
if (fs.existsSync(path.join(aggRoot, d, "day.json")) && fs.existsSync(path.join(aggRoot, d, "topics.json"))) {
snap = {
date: d,
day: JSON.parse(fs.readFileSync(path.join(aggRoot, d, "day.json"), "utf8")),
topics: JSON.parse(fs.readFileSync(path.join(aggRoot, d, "topics.json"), "utf8")),
gis: fs.existsSync(path.join(aggRoot, d, "gis.json")) ? JSON.parse(fs.readFileSync(path.join(aggRoot, d, "gis.json"), "utf8")) : { countries: [] },
events: fs.existsSync(path.join(aggRoot, d, "events.json")) ? JSON.parse(fs.readFileSync(path.join(aggRoot, d, "events.json"), "utf8")).events : [],
essences: fs.existsSync(path.join(EMERGENCE_DATA, "distilled", d, "essence.jsonl"))
? fs.readFileSync(path.join(EMERGENCE_DATA, "distilled", d, "essence.jsonl"), "utf8").split("\n").filter(Boolean).map((l) => JSON.parse(l))
: [],
};
break;
}
}
return snap;
}
const WORLD = JSON.parse(fs.readFileSync(path.join(ASSETS, "emergence-world.json"), "utf8"));
function emergencePage(snap) {
return chrome({
title: `Emergence · live world events, not fact-checked · ${snap.date} · fact.ngo`,
desc: "A day of newsroom reporting, summarized by AI, not fact-checked. Explore coverage, read what remains ambiguous, and inspect every original source.",
body: emergenceBody(snap, WORLD, SITE_REF), theme: "emergence", accent: "#b8d3c4", home: "../index.html",
isFact: true, isEmergence: true,
extraCss: fs.readFileSync(path.join(ASSETS, "emergence.css"), "utf8"),
});
}
/* ---------- CSS ---------- */
const CSS = fs.readFileSync(path.join(ASSETS, "site.css"), "utf8");
const emSnap = loadEmergence();
/* ---------- write ---------- */
fs.rmSync(OUT, { recursive: true, force: true });
fs.mkdirSync(path.join(OUT, "xray", "downloads"), { recursive: true });
fs.writeFileSync(path.join(OUT, "_headers"), `/assets/*
Cache-Control: public, max-age=0, must-revalidate
/emergence/data.json
Cache-Control: public, max-age=0, must-revalidate
`);
fs.cpSync(ASSETS, path.join(OUT, "assets"), { recursive: true });
fs.writeFileSync(path.join(OUT, "index.html"), factHomepage(emSnap));
fs.writeFileSync(path.join(OUT, "xray", "index.html"), xrayLanding());
fs.writeFileSync(path.join(OUT, "xray", "cases.html"), homepage());
for (const m of models) {
fs.writeFileSync(path.join(OUT, "xray", `${m.cfg.slug}.html`), modelPage(m));
fs.cpSync(path.join(DS, m.cfg.slug, "sessions"), path.join(OUT, "xray", "transcripts", m.cfg.slug), { recursive: true });
execSync(`git -C "${path.join(DS, m.cfg.slug)}" archive --format=zip -o "${path.join(OUT, "xray", "downloads", m.cfg.slug + ".zip")}" HEAD`);
}
/* ---------- emergence ---------- */
if (emSnap) {
fs.mkdirSync(path.join(OUT, "emergence"), { recursive: true });
fs.writeFileSync(path.join(OUT, "emergence", "index.html"), emergencePage(emSnap));
fs.writeFileSync(path.join(OUT, "emergence", "data.json"), JSON.stringify({
generated: new Date().toISOString(),
date: emSnap.date,
day: emSnap.day,
topics: emSnap.topics,
gis: emSnap.gis,
events: emSnap.events,
essences: emSnap.essences.filter((e) => !e.failure),
council: { lane: "verified", active: false },
license_note: "Summaries of newsroom teasers, attributed per record; not verified facts.",
}, null, 1) + "\n");
} else {
console.warn("WARN: emergence-data not found or empty; skipping /emergence/ (set EMERGENCE_DATA to override)");
}
const gitRef = (dir) => { try { return execSync(`git -C "${dir}" rev-parse HEAD`, { encoding: "utf8" }).trim(); } catch { return null; } };
fs.writeFileSync(path.join(OUT, "BUILD_MANIFEST.json"), JSON.stringify({
built: new Date().toISOString(),
website: gitRef(path.join(OUT, "..")),
engine: gitRef(XRAY),
datasets: Object.fromEntries(models.map((m) => [m.cfg.slug, gitRef(path.join(DS, m.cfg.slug))])),
...(emSnap ? { emergence_engine: gitRef(path.join(MONO, "emergence")), emergence_data: gitRef(EMERGENCE_DATA), emergence_date: emSnap.date } : {}),
}, null, 2) + "\n");
/* ---------- regression gate ---------- */
// Generated pages may never carry the stale label language (in any record or chrome);
// the internal two-word feed-depth phrase is additionally banned from source templates,
// since record prose may legitimately contain it until aged out.
for (const phrase of ["unverified · AI-distilled", "AI-distilled"]) {
for (const page of ["index.html", path.join("emergence", "index.html"), ...models.map((m) => path.join("xray", m.cfg.slug + ".html")), path.join("xray", "index.html"), path.join("xray", "cases.html")]) {
const p = path.join(OUT, page);
if (fs.existsSync(p) && fs.readFileSync(p, "utf8").includes(phrase)) {
throw new Error(`REGRESSION: stale label "${phrase}" found in ${page}. Fix the source templates; do not ship it.`);
}
}
}
for (const file of ["scripts/emergence-page.js", "assets/emergence.js", "scripts/build.js"]) {
const t = fs.readFileSync(path.join(import.meta.dirname, "..", file), "utf8");
const JARGON = ["teaser", "depth"].join(" ");
if (t.includes(JARGON)) throw new Error(`REGRESSION: internal jargon in ${file}.`);
}
console.log(`built fact.ngo homepage + xray showcase + case-files index + ${models.length} case files + transcripts + ${models.length} dataset zips${emSnap ? ` + emergence (${emSnap.date}: ${emSnap.essences.filter((e) => !e.failure).length} essences)` : ""} into dist/`);
for (const m of models) {
console.log(` xray/${m.cfg.slug}.html (${m.stat.records} records, verbatim ${m.stat.verbatimOk}/${m.stat.records})`);
}