source.fact.ngo a coherence.ngo project

scripts/build.js

raw ↗ · AGPL-3.0

#!/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, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;"); /* ---------- 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&amp;widget=true&amp;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})`); }