/** * Verifies every published Zener trial against its commitment. * * npx tsx analysis/verify-zener.ts # against the live data * npx tsx analysis/verify-zener.ts trials.csv # or a saved file * * This is the script the fairness claim rests on. EXP-003 shows each * participant a SHA-256 hash before they guess, and reveals the card and nonce * afterwards. If hash(card + ":" + nonce) equals the commitment, that card * cannot have been chosen after the guess — the server would have had to find * a second pair colliding under SHA-256. * * Nothing here trusts us: it recomputes every hash from the published file. A * single mismatch would mean a trial's card was altered after the fact, and * this prints it rather than summarising it away. */ import { createHash } from "node:crypto"; import { readFile } from "node:fs/promises"; const DATA_URL = "https://occultresearch.org/api/data/psychic-self-test"; type Trial = { entryDate: string; participantKey: string; commitment: string; nonce: string; card: string; guess: string; isHit: boolean; }; function parseCsv(text: string): Trial[] { const rows: Trial[] = []; for (const line of text.split(/\r?\n/)) { if (!line || line.startsWith("#") || line.startsWith("entry_date")) continue; const cells: string[] = []; let cell = ""; let inQuotes = false; for (let i = 0; i < line.length; i++) { const ch = line[i]; if (inQuotes) { if (ch === '"' && line[i + 1] === '"') { cell += '"'; i++; } else if (ch === '"') inQuotes = false; else cell += ch; } else if (ch === '"') inQuotes = true; else if (ch === ",") { cells.push(cell); cell = ""; } else cell += ch; } cells.push(cell); if (cells.length < 7) continue; rows.push({ entryDate: cells[0], participantKey: cells[1], commitment: cells[2], nonce: cells[3], card: cells[4], guess: cells[5], isHit: cells[6] === "1", }); } return rows; } const sha256 = (input: string) => createHash("sha256").update(input, "utf8").digest("hex"); async function main() { const source = process.argv[2]; const text = source ? await readFile(source, "utf8") : await fetch(DATA_URL).then((r) => { if (!r.ok) throw new Error(`open data unavailable: HTTP ${r.status}`); return r.text(); }); const trials = parseCsv(text); console.log("\nEXP-003 — commitment verification"); console.log("Protocol: protocols/EXP-003.md §4\n"); if (trials.length === 0) { console.log("No trials published yet.\n"); return; } const failures: Trial[] = []; const scoringErrors: Trial[] = []; const cardCounts = new Map(); for (const trial of trials) { if (sha256(`${trial.card}:${trial.nonce}`) !== trial.commitment) failures.push(trial); // The recorded outcome must also match the revealed card and guess. if ((trial.card === trial.guess) !== trial.isHit) scoringErrors.push(trial); cardCounts.set(trial.card, (cardCounts.get(trial.card) ?? 0) + 1); } const hits = trials.filter((t) => t.isHit).length; console.log(`Trials verified ${trials.length.toLocaleString("en-US")}`); console.log(`Commitment mismatches ${failures.length}`); console.log(`Scoring mismatches ${scoringErrors.length}`); console.log(`Observed hit rate ${((hits / trials.length) * 100).toFixed(2)}% (chance 20.00%)`); // §8: a biased generator would look exactly like an effect, so the card // distribution is part of the verification rather than a footnote. console.log("\nDealt-card frequencies (each should sit near 20%)"); for (const [card, count] of [...cardCounts.entries()].sort()) { const share = (count / trials.length) * 100; console.log(` ${card.padEnd(8)} ${String(count).padStart(7)} ${share.toFixed(2)}%`); } if (failures.length > 0) { console.log("\nMISMATCHED COMMITMENTS — each of these is a broken fairness claim:"); for (const trial of failures.slice(0, 20)) { console.log(` ${trial.entryDate} ${trial.participantKey} card=${trial.card} commitment=${trial.commitment}`); } if (failures.length > 20) console.log(` … and ${failures.length - 20} more`); process.exit(1); } console.log("\n → Every commitment matches. No card in this file could have been"); console.log(" chosen after the guess it was scored against.\n"); } main().catch((error) => { console.error(error instanceof Error ? error.message : error); process.exit(1); });