// Goodhart, checked against oracles that are not the engine, and then broken // on purpose. // // The page's arithmetic is three rank scores and two closed forms. Each is // computed here a second way that shares no code with the engine: // // - Kendall's tau by counting inversions with a merge sort, not by walking // every pair // - Spearman's rho from ranks found by counting, not by sorting, through // both the d-squared formula and Pearson's correlation of the ranks // - both again by scipy, a third implementation in another language // - the closed forms against a Monte Carlo sweep driven by this file's own // generator, and against worked values parsed out of the archived // tutorial that states them // // The page's prose numbers are pinned as literals at the end, because the // claims gate reads this file as text. import { readFileSync, existsSync } from 'node:fs'; import { execFileSync } from 'node:child_process'; import { createHash } from 'node:crypto'; import { fileURLToPath } from 'node:url'; import { dirname, join } from 'node:path'; import { homedir } from 'node:os'; const HERE = dirname(fileURLToPath(import.meta.url)); const ROOT = join(HERE, '..'); let p = 0; let f = 0; const check = (label, ok, detail = '') => { if (ok) { p += 1; console.log(` ok ${label}${detail ? ` [${detail}]` : ''}`); } else { f += 1; console.error(` FAIL ${label}${detail ? `\n ${detail}` : ''}`); } }; const ENGINE = join(ROOT, 'site/js/goodhart.js'); function engine(src = readFileSync(ENGINE, 'utf8')) { const window = {}; new Function('window', src)(window); return window.Goodhart; } const G = engine(); const D = G.DEFAULTS; /* ---- the engine's own claims ------------------------------------------ */ const notes = G.verify(); check('the page makes at least nine checks at load', notes.length >= 9, String(notes.length)); notes.forEach((n) => check(`load-time: ${n.claim.slice(0, 70)}`, n.ok && !n.untested, n.detail)); /* ---- a second implementation of every score ---------------------------- */ // Ranks by counting how many values are smaller, and how many are equal. // O(n^2), and nothing like the engine's sort. function ranksByCounting(xs) { return xs.map((x) => { let below = 0; let same = 0; for (const y of xs) { if (y < x) below += 1; else if (y === x) same += 1; } return below + (same + 1) / 2; }); } function pearsonPlain(a, b) { const n = a.length; const ma = a.reduce((t, x) => t + x, 0) / n; const mb = b.reduce((t, x) => t + x, 0) / n; let num = 0; let da = 0; let db = 0; for (let i = 0; i < n; i += 1) { num += (a[i] - ma) * (b[i] - mb); da += (a[i] - ma) ** 2; db += (b[i] - mb) ** 2; } return num / Math.sqrt(da * db); } function spearmanD2(x, y) { const rx = ranksByCounting(x); const ry = ranksByCounting(y); const n = x.length; let s = 0; for (let i = 0; i < n; i += 1) s += (rx[i] - ry[i]) ** 2; return 1 - (6 * s) / (n * (n * n - 1)); } // Kendall's tau-a by inversions: order by quality, then count how many pairs // the number puts the other way, with a merge sort. No ties assumed, and the // caller checks there are none. function inversions(arr) { if (arr.length < 2) return [arr, 0]; const mid = arr.length >> 1; const [l, a] = inversions(arr.slice(0, mid)); const [r, b] = inversions(arr.slice(mid)); const out = []; let i = 0; let j = 0; let c = a + b; while (i < l.length && j < r.length) { if (l[i] <= r[j]) out.push(l[i++]); else { out.push(r[j++]); c += l.length - i; } } return [out.concat(l.slice(i), r.slice(j)), c]; } function kendallByInversions(x, y) { const idx = y.map((_, i) => i).sort((a, b) => y[a] - y[b]); const [, inv] = inversions(idx.map((i) => x[i])); const n = x.length; const pairs = (n * (n - 1)) / 2; return { tau: 1 - (2 * inv) / pairs, wrong: inv, right: pairs - inv }; } const distinct = (xs) => new Set(xs).size === xs.length; { const SETTINGS = [ { sigma: 0, who: 'bottom', share: 0.7, boost: 2 }, { sigma: 0.5, who: 'bottom', share: 0.7, boost: 2 }, { sigma: 1, who: 'anyone', share: 0.3, boost: 3 }, { sigma: 2, who: 'bottom', share: 1, boost: 0.5 }, ]; let compared = 0; let bad = 0; const worst = []; for (const s of SETTINGS) { for (let seed = 1; seed <= 60; seed += 1) { const m = G.run({ seed, ...s }); const states = [ [m.pop.proxy, m.pop.quality, m.before], [m.route.proxy, m.route.quality, m.after], [m.better.proxy, m.better.quality, m.control], ]; for (const [x, y, sc] of states) { if (!distinct(x) || !distinct(y)) continue; compared += 1; const k = kendallByInversions(x, y); const r1 = spearmanD2(x, y); const r2 = pearsonPlain(ranksByCounting(x), ranksByCounting(y)); const errs = [Math.abs(k.tau - sc.tau), Math.abs(r1 - sc.rhoSquares), Math.abs(r2 - sc.rhoRanks), Math.abs(r1 - r2)]; if (k.right !== sc.inOrder || k.wrong !== sc.outOfOrder || errs.some((e) => !(e < 1e-12))) { bad += 1; worst.push(seed); } } } } check('every score agrees with a second implementation, over 720 populations', compared === 720 && bad === 0, `${compared} compared, ${bad} disagree ${worst.slice(0, 3)}`); } /* ---- and with a third, in another language ----------------------------- */ { const cases = []; for (let seed = 1; seed <= 40; seed += 1) { const m = G.run({ ...D, seed }); cases.push({ x: m.pop.proxy, y: m.pop.quality, tau: m.before.tau, rho: m.before.rhoRanks }); cases.push({ x: m.route.proxy, y: m.route.quality, tau: m.after.tau, rho: m.after.rhoRanks }); } let out = null; try { out = execFileSync('python3', ['-c', ` import json, sys from scipy.stats import kendalltau, spearmanr cs = json.load(sys.stdin) print(json.dumps([[kendalltau(c["x"], c["y"])[0], spearmanr(c["x"], c["y"])[0]] for c in cs])) `], { input: JSON.stringify(cases), encoding: 'utf8' }); } catch (e) { check('scipy is available to witness the scores', false, `NOT TESTED, and counted as a failure: ${String(e.message).split('\n')[0]}`); } if (out) { const sp = JSON.parse(out); const bad = sp.filter(([t, r], i) => !(Math.abs(t - cases[i].tau) < 1e-12) || !(Math.abs(r - cases[i].rho) < 1e-12)).length; check('scipy computes the same tau and rho for 80 orderings', sp.length === 80 && bad === 0, `${sp.length} compared, ${bad} disagree`); } } /* ---- the tie rule ------------------------------------------------------ */ check('the squared-difference formula refuses a tie rather than approximate', G.spearmanBySquares([1, 2, 2, 3], [1, 2, 3, 4]) === null && G.spearmanByRanks([1, 2, 2, 3], [1, 2, 3, 4]) !== null); check('and on the same tie the two Spearman routes would really differ, so the refusal matters', Math.abs(pearsonPlain(ranksByCounting([1, 2, 2, 3]), [1, 2, 3, 4]) - spearmanD2([1, 2, 2, 3], [1, 2, 3, 4])) > 1e-6); check('ties are given the average of the places they share', G.ranks([5, 1, 5, 3]).join() === '3.5,1,3.5,2', G.ranks([5, 1, 5, 3]).join()); /* ---- the closed forms, by a route that shares nothing with the engine --- */ // xorshift128+ halves and Marsaglia's polar method: a different generator and // a different way of making a normal from the ones the engine uses. function polarStream(seed) { let s0 = BigInt(seed) * 0x9E3779B97F4A7C15n & 0xFFFFFFFFFFFFFFFFn | 1n; let s1 = (BigInt(seed) + 0x632BE59BD9B4E019n) & 0xFFFFFFFFFFFFFFFFn; const u = () => { let x = s0; const y = s1; s0 = y; x ^= (x << 23n) & 0xFFFFFFFFFFFFFFFFn; x ^= x >> 17n; x ^= y ^ (y >> 26n); s1 = x; return Number(((s0 + s1) & 0xFFFFFFFFFFFFFFFFn) >> 11n) / 2 ** 53; }; let spare = null; return () => { if (spare !== null) { const v = spare; spare = null; return v; } for (;;) { const a = 2 * u() - 1; const b = 2 * u() - 1; const s = a * a + b * b; if (s > 0 && s < 1) { const k = Math.sqrt(-2 * Math.log(s) / s); spare = b * k; return a * k; } } }; } { const M = 600; const rows = []; for (const [n, sigma] of [[50, 0.5], [50, 1.5], [12, 0.75], [30, 0]]) { const z = polarStream(20261007 + n * 100 + Math.round(sigma * 100)); const taus = []; const rhos = []; for (let k = 0; k < M; k += 1) { const q = []; const x = []; for (let i = 0; i < n; i += 1) { const a = z(); q.push(a); x.push(a + sigma * z()); } taus.push(kendallByInversions(x, q).tau); rhos.push(spearmanD2(x, q)); } const st = (xs) => { const m = xs.reduce((t, v) => t + v, 0) / xs.length; const v = xs.reduce((t, w) => t + (w - m) ** 2, 0) / (xs.length - 1); return { m, se: Math.sqrt(v / xs.length) }; }; const e = G.expected(n, sigma); const t = st(taus); const r = st(rhos); // At sigma 0 both are exactly 1 and the standard error is 0. const near = (s, want) => (s.se === 0 ? Math.abs(s.m - want) < 1e-12 : Math.abs(s.m - want) / s.se < G.SWEEP.standardErrors); rows.push([n, sigma, near(t, e.tau), near(r, e.rho), `tau ${t.m.toFixed(4)} vs ${e.tau.toFixed(4)}, rho ${r.m.toFixed(4)} vs ${e.rho.toFixed(4)}`]); } rows.forEach(([n, sigma, okT, okR, d]) => check(`n=${n}, error ${sigma}: both closed forms hold against an independent sweep`, okT && okR, d)); // The formula has to be able to fail this: Moran's with (n - 1) in place of // (n - 2) is off by asin(r/2) * 6 / (pi (n + 1)), which at n = 12 is 0.07, // dozens of standard errors. // The formulas themselves, typed here from the tutorial's equations 7 and 10 // rather than read from the engine, over a grid. let formulaOff = 0; for (const n of [5, 12, 20, 50, 200]) { for (let s = 0; s <= 3; s += 0.25) { const r = 1 / Math.sqrt(1 + s * s); const e = G.expected(n, s); const tau = (2 / Math.PI) * Math.asin(r); const rho = (6 / Math.PI) / (n + 1) * (Math.asin(r) + (n - 2) * Math.asin(r / 2)); if (Math.abs(e.tau - tau) > 1e-14 || Math.abs(e.rho - rho) > 1e-14 || Math.abs(e.r - r) > 1e-15) formulaOff += 1; } } check('the engine\'s closed forms are the tutorial\'s, over 65 grid points', formulaOff === 0, String(formulaOff)); const wrong = (n, r) => 6 / (Math.PI * (n + 1)) * (Math.asin(r) + (n - 1) * Math.asin(r / 2)); const e12 = G.expected(12, 0.75); check('and a wrong version of Moran\'s formula would be far outside the tolerance', Math.abs(wrong(12, e12.r) - e12.rho) > 0.05, (wrong(12, e12.r) - e12.rho).toFixed(4)); } /* ---- the published worked values, read out of the archived tutorial ---- */ const spec = JSON.parse(readFileSync(join(ROOT, 'machines/goodhart.json'), 'utf8')); function sourceText(part) { const url = spec.sources.map((s) => s.url).find((u) => u.includes(part)); if (!url) return { url: null, text: null, why: `no source on the page matches ${part}` }; const manifest = join(ROOT, 'docs/sources/manifest.json'); if (existsSync(manifest)) { const e = JSON.parse(readFileSync(manifest, 'utf8')).sources .find((m) => m.url === url && m.status === 'ok' && m.file); if (e && existsSync(join(ROOT, 'docs/sources', e.file))) { return { url, text: readFileSync(join(ROOT, 'docs/sources', e.file), 'utf8') }; } } // Development only: a scratch copy fetched by webget.py and named the way // la-source-audit names its snapshots. if (process.env.LA_SOURCES_DIR) { const slug = createHash('sha256').update(url).digest('hex').slice(0, 12); const file = join(process.env.LA_SOURCES_DIR, `${slug}.txt`); if (existsSync(file)) return { url, text: readFileSync(file, 'utf8') }; } return { url, text: null, why: 'archive missing: run la-source-audit --archive' }; } const SMART = { '‘': "'", '’': "'", '“': '"', '”': '"', '': '' }; const norm = (s) => s.replace(/[‘’“”]/g, (c) => SMART[c]) .replace(/\s+/g, ' ').replace(/\s+([,.;:!?)])/g, '$1').replace(/([(])\s+/g, '$1') .trim().toLowerCase(); { const tut = sourceText('2408.15979'); check('the correlation tutorial is archived', Boolean(tut.text), tut.why || ''); if (tut.text) { const t = tut.text.replace(/\s+/g, ' '); // "if Rp = .2 (corresponding Rs = .191, ...), then E(rp) and E(rs) are .177 // and .160, respectively at N = 5, and .195 and .182 at N = 20." // "If Rp = .8 (Rs = .786), then E(rp) and E(rs) are .754 and .688 at N = 5, // and .792 and .758 at N = 20." const re = /Rp = (\.\d+) \((?:corresponding )?Rs = (\.\d+)[^)]*\), then E\(rp\) and E\(rs\) are \.\d+ and (\.\d+),? (?:respectively )?at N = (\d+), and \.\d+ and (\.\d+) at N = (\d+)/g; const found = [...t.matchAll(re)]; check('two worked examples are parsed out of it', found.length === 2, String(found.length)); for (const m of found) { const r = Number(m[1]); const sigma = Math.sqrt(1 / (r * r) - 1); const rsInf = G.expected(1e9, sigma).rho; const a = G.expected(Number(m[4]), sigma).rho; const b = G.expected(Number(m[6]), sigma).rho; check(`Rp ${m[1]}: the population Spearman is ${m[2]}, as printed`, rsInf.toFixed(3).slice(1) === m[2], rsInf.toFixed(4)); check(`Rp ${m[1]}: Moran's expectation at N = ${m[4]} is ${m[3]}, as printed`, a.toFixed(3).slice(1) === m[3], a.toFixed(4)); check(`Rp ${m[1]}: and at N = ${m[6]} it is ${m[5]}, as printed`, b.toFixed(3).slice(1) === m[5], b.toFixed(4)); } // "The maximum difference between Rp and Rs is .0181 and occurs at Rp = .594" const mx = t.match(/maximum difference between Rp and Rs is (\.\d+) and occurs at Rp = (\.\d+)/); check('the tutorial states where Pearson and Spearman differ most', Boolean(mx)); if (mx) { // Two routes: a fine scan, and the stationary point in closed form, // where the slope of (6/pi) asin(r/2) is 1: r = 2 sqrt(1 - 9/pi^2). let best = 0; let at = 0; for (let k = 1; k < 1000000; k += 1) { const r = k / 1000000; const gap = r - (6 / Math.PI) * Math.asin(r / 2); if (gap > best) { best = gap; at = r; } } const star = 2 * Math.sqrt(1 - 9 / (Math.PI * Math.PI)); const gapStar = star - (6 / Math.PI) * Math.asin(star / 2); check(`and a scan of (6/pi) asin(r/2) finds ${mx[1]} at ${mx[2]}`, best.toFixed(4).slice(1) === mx[1] && at.toFixed(3).slice(1) === mx[2], `${best.toFixed(4)} at ${at.toFixed(4)}`); check('and the closed-form stationary point agrees with the scan', Math.abs(star - at) < 2e-6 && gapStar.toFixed(4).slice(1) === mx[1], `${star.toFixed(6)}, gap ${gapStar.toFixed(5)}`); } } } /* ---- every number honest, and the control ------------------------------ */ { let runs = 0; let dishonest = 0; let controlBroken = 0; let spreadOff = 0; let narrowedBoth = 0; let rankLost = 0; let everyoneMoved = 0; for (let seed = 1; seed <= 200; seed += 1) { for (const who of ['bottom', 'anyone']) { const m = G.run({ ...D, seed, who }); runs += 1; if (m.honest !== m.pop.n) dishonest += 1; if (m.control.inOrder !== m.before.inOrder || m.control.tau !== m.before.tau || m.control.rhoRanks !== m.before.rhoRanks) controlBroken += 1; if (Math.abs(m.control.spread - m.after.spread) > 1e-9) spreadOff += 1; if (who === 'bottom' && m.after.spread < m.before.spread) narrowedBoth += 1; if (m.after.inOrder < m.before.inOrder) rankLost += 1; const all = G.run({ ...D, seed, who: 'anyone', share: 1 }); if (all.after.inOrder !== all.before.inOrder) everyoneMoved += 1; } } check('over 400 runs every number stays a true reading', runs === 400 && dishonest === 0, `${dishonest} of ${runs} not`); check('and the control keeps every pair in place every time', controlBroken === 0, `${controlBroken} of ${runs} moved`); check('and ends at exactly the cheap route\'s spread', spreadOff === 0, `${spreadOff} off`); check('a route everyone takes changes no ranking, in all 400', everyoneMoved === 0, String(everyoneMoved)); // Not a guarantee, so measured and bounded rather than asserted always. check('at the page\'s settings the cheap route costs pairs in nearly every draw', rankLost >= 0.95 * runs, `${rankLost} of ${runs}`); check('and narrows the spread when the lower half take it, in most draws', narrowedBoth > runs / 4, `${narrowedBoth} of ${runs / 2} draws`); // Raising the share only ever adds takers. let notMonotone = 0; for (let seed = 1; seed <= 100; seed += 1) { let prev = null; for (let s = 0; s <= 10; s += 1) { const r = G.cheapRoute(G.population({ seed, sigma: D.sigma }), { ...D, share: s / 10 }); if (prev && prev.took.some((t, i) => t && !r.took[i])) notMonotone += 1; prev = r; } } check('raising the share only adds takers, never swaps them', notMonotone === 0, String(notMonotone)); // The ledger's third case: improvement that leaves the error at its old // size. Same narrowing, no second route, and on average the order erodes. let erode = 0; let total = 0; let sumBefore = 0; let sumAfter = 0; for (let seed = 1; seed <= 300; seed += 1) { const pop = G.population({ seed, sigma: 1 }); const q2 = G.stretch(pop.quality, 0.6); const x2 = q2.map((q, i) => q + pop.sigma * pop.noise[i]); const a = G.pairs(pop.proxy, pop.quality).concordant; const b = G.pairs(x2, q2).concordant; total += 1; sumBefore += a; sumAfter += b; if (b < a) erode += 1; } check('improvement that leaves the error alone erodes the order a little, on average', sumAfter < sumBefore && erode > total / 2, `${(sumBefore / total).toFixed(1)} to ${(sumAfter / total).toFixed(1)} pairs; fewer in ${erode} of ${total}`); } /* ---- the numbers the page opens on ------------------------------------ */ // // Pinned as literals because the claims gate reads this file as text, and // computed here from the engine so a literal cannot outlive its value. { const m = G.run(D); const per100 = (a, b) => Math.round((100 * a) / b); const OPEN = [ ['draw', String(m.pop.seed), '1975'], ['correlation', m.expected.r.toFixed(3), '0.894'], ['Spearman by ranks', m.before.rhoRanks.toFixed(3), '0.895'], ['Spearman by squares', m.before.rhoSquares.toFixed(3), '0.895'], ['Kendall', m.before.tau.toFixed(3), '0.740'], ['pairs in order', `${m.before.inOrder} of ${m.before.pairs}`, '1066 of 1225'], ['in 100, before', String(per100(m.before.inOrder, m.before.pairs)), '87'], ['expected tau', m.expected.tau.toFixed(3), '0.705'], ['expected rho', m.expected.rho.toFixed(3), '0.875'], ['could take it', `${m.route.eligible} of ${m.pop.n}`, '25 of 50'], ['took it', String(m.route.takers), '18'], ['true readings', `${m.honest} of ${m.pop.n}`, '50 of 50'], ['spread before', m.before.spread.toFixed(3), '1.162'], ['spread after', m.after.spread.toFixed(3), '1.000'], ['spread, control', m.control.spread.toFixed(3), '1.000'], ['rho after', m.after.rhoRanks.toFixed(3), '0.409'], ['tau after', m.after.tau.toFixed(3), '0.301'], ['pairs after', `${m.after.inOrder} of ${m.after.pairs}`, '797 of 1225'], ['in 100, after', String(per100(m.after.inOrder, m.after.pairs)), '65'], ['pairs lost', String(m.before.inOrder - m.after.inOrder), '269'], ['spread narrowed, per cent', String(Math.round(Math.abs(1 - m.ratio) * 100)), '14'], ['error', m.settings.sigma.toFixed(2), '0.50'], ['share', `${Math.round(m.settings.share * 100)} per cent`, '70 per cent'], ['boost', m.settings.boost.toFixed(1), '2.0'], ['pairs of fifty', String((50 * 49) / 2), '1225'], ]; for (const [what, got, want] of OPEN) check(`the page opens with ${what} ${want}`, got === want, got); check('a coin puts half the pairs in order, 50 in 100', per100(612.5, 1225) === 50); check('the load-time sweep is 400 populations at four standard errors', G.SWEEP.populations === 400 && G.SWEEP.standardErrors === 4); check('fifty members', G.N === 50 && m.pop.n === 50); } /* ---- the sources say what the page says -------------------------------- */ { const page = JSON.stringify(spec); const QUOTES = [ ['any observed statistical regularity will tend to collapse once pressure is placed upon it for control purposes', ['PMC7901608', '1803.04585']], ['the statement of the law was a (jocular) aside rather than the main point of the paper', ['wgak/qeka']], ['Ignoring Goodhart’s law', ['wgak/qeka']], ['in a volume edited by Courakis (1981) and then again in a volume of Charles’ own papers (Goodhart, 1984)', ['wgak/qeka']], ['When a measure becomes a target, it ceases to be a good measure.', ['1997-strathern']], ['The more a 2.1 examination performance becomes an expectation, the poorer it becomes as a discriminator of individual performances.', ['1997-strathern']], ['Revised version of seven papers presented at the Conference in Monetary Economics, Sydney, July 1975, and two additional papers.', ['rdp/1990/9013']], ['This paper contains the first reference to what became known as', ['rdp/1990/9013']], ['A spread that bunches up is a reason to look, not a verdict', ['the-proof-was-never-the-point']], ['When selecting for a proxy measure, you select not only for the true goal, but also for the difference between the proxy and the goal.', ['1803.04585']], ['which arguably has scholarly precedence', ['1803.04585']], ]; for (const [q, where] of QUOTES) { const unent = page.replace(/’/g, '’').replace(/‘/g, '‘'); check(`the page quotes "${q.slice(0, 48)}"`, norm(unent).includes(norm(q))); for (const part of where) { const s = sourceText(part); check(` and ${part} says it`, Boolean(s.text) && norm(s.text).includes(norm(q)), s.text ? '' : s.why); } } const rba = sourceText('rdp/1990/9013'); if (rba.text) { const t = norm(rba.text); check('the bank\'s bibliography dates the printed volume 1976', t.includes('papers in monetary economics. vol. i and ii. sydney: reserve bank of australia, 1976.')); check('and puts the law\'s first reference in Problems of Monetary Management', /problems of monetary management\s*: the u\.k\. experience \[36\]/.test(t)); } else check('the bank\'s bibliography is archived', false, rba.why); const strathern = sourceText('1997-strathern'); if (strathern.text) { check('Strathern credits Hoskin with the name, which the ledger does not contradict', norm(strathern.text).includes(norm("Hoskin describes this as 'Goodhart's law'"))); check('and the passage is in European Review 5, 305-321, 1997', /Eur\. Rev\., 5, 305-321 \(1997\)/.test(strathern.text)); } } /* ---- what the page and the roster entry say ---------------------------- */ { const rosterFile = join(homedir(), 'projects/siteops/shared/logical-art/machines.json'); const shared = JSON.parse(readFileSync(rosterFile, 'utf8')).machines.find((x) => x.id === 'goodhart'); const proposed = join(ROOT, 'session-notes/roster-goodhart.json'); const entry = shared && shared.status === 'live' ? shared : existsSync(proposed) ? JSON.parse(readFileSync(proposed, 'utf8')) : null; check('a live roster entry exists, shared or proposed', Boolean(entry)); const pageFile = join(ROOT, 'site/nibbles/goodhart/index.html'); check('the page exists', existsSync(pageFile)); if (entry && existsSync(pageFile)) { const html = readFileSync(pageFile, 'utf8'); const heads = [...html.matchAll(/
\d<\/span> ([^<]+)<\/p>/g)] .map((m) => m[1].replace(/'/g, "'").replace(/&/g, '&')); check('the page renders exactly the roster\'s four stages, in order', heads.length === 4 && heads.join('|') === entry.stages.join('|'), heads.join(' | ')); check('the era is 1975', entry.era === '1975' && /1975<\/span>/.test(html)); check('the page points at PageRank rather than re-deriving it', html.includes('href="/nibbles/pagerank/"') && !/1 - d|\(1-d\)|damping/i.test(html)); const flat = html.replace(/<[^>]+>/g, ' ').replace(/&[a-z]+;/g, ' ').replace(/\s+/g, ' '); // Superlatives, proved before they are believed. // Assertion-shaped, because the ledger entry that refuses the claim is // titled "Not the first statement of the idea" and a vocabulary search // cannot tell the two apart. const claimsFirst = (t) => /\b(? spec.ledger.find((e) => e.summary.startsWith(s)).body; // The control, over every setting the page offers, on several draws: nobody // gets worse, and it widens as often as it narrows. So the prose has to // describe both anchors, not only the best. let states = 0; let worse = 0; let widened = 0; let flat = 0; for (const seed of [1975, 1976, 7, 42]) { for (const sigma of [0, 0.5, 1, 2]) { for (const who of ['bottom', 'anyone']) { for (const share of [0, 0.3, 0.7, 1]) { for (const boost of [0, 0.5, 2, 4]) { const m = G.run({ seed, sigma, who, share, boost }); states += 1; if (m.ratio > 1 + 1e-9) widened += 1; if (Math.abs(m.ratio - 1) <= 1e-9) flat += 1; for (let i = 0; i < m.pop.n; i += 1) { if (m.better.quality[i] < m.pop.quality[i] - 1e-12 || m.better.proxy[i] < m.pop.proxy[i] - 1e-12) { worse += 1; break; } } } } } } } check('over every corner of the controls, nobody in the control gets worse', worse === 0, `${worse} of ${states} states`); check('and the control widens in some states and stands still in others', widened > 0 && flat > 0, `${widened} widened, ${flat} unchanged, of ${states}`); check('so the ledger names both anchors, the best and the worst', /anchored at the best, so the weakest gain most; when it has to widen it is anchored at the worst/.test(ledger('The control keeps'))); check('and the stage 4 hint does not say the weakest always gain most', /the weakest gain most when the spread narrows, the strongest when it widens/.test(spec.panels[3])); // After the cheap route the order is worse, not gone: more than half the // pairs are still right at the page's settings. const m = G.run(D); check('the cheap route still ranks most pairs correctly, so the ledger may not say otherwise', m.after.inOrder > m.after.pairs / 2 && !/ranks anybody correctly/.test(text), `${m.after.inOrder} of ${m.after.pairs}`); // 0.894 is the population's correlation; the drawn fifty's is another number. const realised = G.pearson(m.pop.proxy, m.pop.quality); check('the drawn fifty correlate at a value other than the population\'s 0.894', realised.toFixed(3) !== m.expected.r.toFixed(3), realised.toFixed(3)); check('so the readout calls 0.894 the population\'s correlation', /correlation of the number with the quality, in the population drawn from/.test(spec.panels[0])); // Spread is the standard deviation over the fifty, and the page says so. const sd = Math.sqrt(m.pop.proxy.reduce((t2, x) => { const mu = m.pop.proxy.reduce((a, b) => a + b, 0) / m.pop.n; return t2 + (x - mu) ** 2; }, 0) / m.pop.n); check('spread is the standard deviation, and stage 1 defines it that way', Math.abs(sd - m.before.spread) < 1e-12 && /Spread, here and below, is the standard deviation/.test(spec.panels[0]), sd.toFixed(6)); check('the closed forms are stated for normal variables, with asin(r/2) bracketed', /two normal variables with correlation r/.test(spec.panels[1]) && /asin\(r\/2\)/.test(spec.panels[1])); // The sweep rows are a tolerance, so their claims say so. const sweepRows = notes.filter((n) => /closed form/.test(n.claim)); check('the two sweep rows claim a tolerance, not an equality', sweepRows.length === 2 && sweepRows.every((n) => /within 4 standard errors of/.test(n.claim)), sweepRows.map((n) => n.claim).join(' / ')); // The tutorial's year, from the archived copy's own citation line. const dw = sourceText('2408.15979'); const year = dw.text && (dw.text.match(/De Winter, J\. C\. F\., Gosling, S\. D\., & Potter, J\. \((\d{4})\)/) || [])[1]; const entry = spec.sources.find((s) => s.url.includes('2408.15979')).text; check(`the de Winter tutorial is dated as its citation line dates it, ${year}`, Boolean(year) && entry.includes(`Psychological Methods 21, 273–290, ${year}`), entry.slice(-90)); check('the lede does not say the page is the sentence and nothing else', !/and nothing else/.test(spec.lede)); // The PageRank page is built on a page dividing its rank between its // links, and on a graph where the answer disagrees with counting links. So // this sentence names the division and does not call the links counted. check('PageRank is described as shares of rank divided by the links a page makes', /a share of the linking page’s own rank, divided by the number of links that page makes/ .test(spec.panels[3]) && !/weighted by the rank of the page it comes from|anybody can count|a number everybody can see/ .test(spec.panels[3]), spec.panels[3].match(/PageRank<\/a>[^<]*/)?.[0]); // Strathern's rewording sits in a passage she says draws on Hoskin, and her // next sentence is about a mark that stops telling candidates apart, which // is not the reordering the comparison table counts. check('the Strathern caption names Hoskin and does not say her 2.1 sentence is what the table measures', /draws on the educationalist Hoskin/.test(spec.panels[3]) && /credit the name Goodhart’s law to him/.test(spec.panels[3]) && !/the one this table measures/.test(spec.panels[3])); } /* ---- break it on purpose ---------------------------------------------- */ { const src = readFileSync(ENGINE, 'utf8'); const cases = [ ['Kendall counting a pair the wrong way round', ['if (a === b) c += 1; else d += 1;', 'if (a !== b) c += 1; else d += 1;']], ['Moran\'s formula with n - 1 for n - 2', ['(Math.asin(r) + (n - 2) * Math.asin(r / 2))', '(Math.asin(r) + (n - 1) * Math.asin(r / 2))']], ['the squared-difference formula with 5 for 6', ['return 1 - 6 * d2 / (n * (n * n - 1));', 'return 1 - 5 * d2 / (n * (n * n - 1));']], ['a cheap route that also lowers the quality of whoever takes it', ['return { quality: pop.quality, proxy: proxy,', 'return { quality: pop.quality.map(function (q, i) { return took[i] ? q - 0.1 : q; }), proxy: proxy,']], ['a control that is not strictly increasing', ['return xs.map(function (x) { return anchor + s * (x - anchor); });', 'return xs.map(function (x) { return anchor + s * (x - anchor) + (x > 0 ? 0.4 : 0); });']], ['a number that does not depend on the quality at all', ['proxy.push(q + sigma * e);', 'proxy.push(sigma * e);']], ['a correlation computed for the wrong error', ['return 1 / Math.sqrt(1 + sigma * sigma);', 'return 1 / Math.sqrt(1 + sigma);']], // The 2026-10-08 red team: a control that raises only the number keeps // every rank and the spread, so nothing but a reading check can see it. ['a control that raises the number and leaves the quality where it was', ['return { quality: stretch(pop.quality, s, anchor),', 'return { quality: pop.quality,']], ]; for (const [label, [from, to]] of cases) { if (!src.includes(from)) { check(`${label} is caught by the page itself`, false, 'the mutation did not match the engine, so nothing was tested'); continue; } let bad; try { bad = engine(src.replace(from, to)).verify().filter((n) => !n.ok); } catch (e) { check(`${label} is caught by the page itself`, false, `verify() threw instead of returning a failed row: ${e.message}`); continue; } check(`${label} is caught by the page itself`, bad.length > 0, `${bad.length} claim(s) go red`); } check('an unmutated copy still passes every one of its own checks', engine(src).verify().every((n) => n.ok)); // A route nobody takes is not a pass: the rows that depend on it say so. const none = engine(src.replace('share: 0.7,', 'share: 0,')).verify(); const untested = none.filter((n) => n.untested); check('with nobody taking the route, the rows about it are not tested rather than passed', untested.length >= 2 && untested.every((n) => n.ok === false), `${untested.length} not tested`); } // ---- which pairs changed, not only how many ------------------------------ // The net fall in the count hides pairs that went the other way. At the // default 288 pairs leave the right order and 19 join it, a net 269; at share // 10 and boost 0.5 eight go each way and the count does not move. Counted // here pair by pair against the quality, sharing no code with the engine. { const G = engine(); const count = (q, b, a) => { let out = 0; let into = 0; for (let i = 0; i < q.length; i += 1) { for (let j = 0; j < q.length; j += 1) { if (i === j || !(q[i] > q[j])) continue; const was = b[i] > b[j]; const is = a[i] > a[j]; if (was && !is) out += 1; if (!was && is) into += 1; } } return { out, into }; }; const d = G.run(G.DEFAULTS); const dc = count(d.pop.quality, d.pop.proxy, d.route.proxy); check('the default route sends 288 pairs out of order and 19 into it', dc.out === 288 && dc.into === 19 && d.moved?.lost === 288 && d.moved?.gained === 19, `oracle ${dc.out}/${dc.into}, engine ${d.moved?.lost}/${d.moved?.gained}`); const z = G.run({ ...G.DEFAULTS, share: 0.1, boost: 0.5 }); const zc = count(z.pop.quality, z.pop.proxy, z.route.proxy); check('a route can leave the count unchanged and still move pairs: 8 out, 8 in', z.before.inOrder === z.after.inOrder && zc.out === 8 && zc.into === 8 && z.moved?.lost === 8 && z.moved?.gained === 8, `${z.before.inOrder}/${z.after.inOrder}, ${zc.out}/${zc.into}`); let states = 0; let agree = 0; let identity = 0; for (let seed = 1975; seed < 1979; seed += 1) { for (const sigma of [0, 0.5, 1, 2]) { for (const who of ['bottom', 'anyone']) { for (const share of [0, 0.3, 0.7, 1]) { for (const boost of [0, 0.5, 2, 4]) { const m = G.run({ seed, sigma, who, share, boost }); const c = count(m.pop.quality, m.pop.proxy, m.route.proxy); states += 1; const mv = m.moved || {}; if (c.out === mv.lost && c.into === mv.gained) agree += 1; if (m.before.inOrder - mv.lost + mv.gained === m.after.inOrder) identity += 1; } } } } } check('the engine\'s pair changes match the oracle in every state of a grid', states === 512 && agree === states, `${agree} of ${states}`); check('and before, less those lost, plus those gained, is after, in every state', identity === states, `${identity} of ${states}`); // The row about a route everyone takes says no ranking changed. A count of // pairs cannot show that: a per-member wobble of 0.01 on the boost reorders // the fifty and leaves 1,066 in order. The row has to go red on it. const src = readFileSync(ENGINE, 'utf8'); const from = 'proxy.push(does ? pop.proxy[i] + o.boost : pop.proxy[i]);'; const W = engine(src.replace(from, 'proxy.push(does ? pop.proxy[i] + o.boost + 0.01 * u : pop.proxy[i]);')); const wa = W.run({ ...W.DEFAULTS, who: 'anyone', share: 1 }); const row = W.verify().find((r) => /every one of them takes/.test(r.claim)); check('a reorder that keeps the count is caught by the row about a universal route', src.includes(from) && wa.after.inOrder === wa.before.inOrder && W.ranks(wa.route.proxy).join() !== W.ranks(wa.pop.proxy).join() && row && row.ok === false, row ? `${wa.after.inOrder} in order, row ok ${row.ok}` : 'row not found'); } /* ---- round three: what the 2026-10-08 red team found ------------------ */ { const src = readFileSync(ENGINE, 'utf8'); // The checks table runs once, on the opening settings, and its first row // says so in words built from DEFAULTS. Change the defaults in a copy and // the row follows, so the words are derived, not typed. const first = notes[0]; check('the first row of the checks table is a statement naming the state it checked', first.stated === true && first.ok === true && first.claim.includes(`draw ${D.seed} at the opening settings: error ${D.sigma}, the lower half by the number can take the cheap route, ${Math.round(D.share * 100)} per cent of them do, boost ${D.boost}`) && /these rows do not describe it/.test(first.claim), first.claim); { const from = 'seed: 1975,'; const moved = src.includes(from) ? engine(src.replace(from, 'seed: 1976,')).verify() : []; check('and with other defaults it names those instead', moved.length > 0 && moved[0].claim.includes('draw 1976 at the opening settings') && moved.filter((n) => /draw 1976/.test(n.claim)).length >= 4, moved[0]?.claim.slice(0, 80) || 'no match'); } check('every computed row on a single draw names it', notes.filter((n) => /^On draw /.test(n.claim)).length === 3); // The "fewer pairs" row at its boundary. At share 10 and boost 0.5 on draw // 1975 the route takes 8 pairs out and puts 8 in, so the count is unchanged // and the row must say NO. A row loosened from < to <= says yes here. const atEdge = (s) => s.replace('share: 0.7,', 'share: 0.1,').replace('boost: 2 ', 'boost: 0.5 '); const edgeSrc = atEdge(src); const rowOf = (vs) => vs.find((n) => /fewer pairs are in the right order/.test(n.claim)); const E = engine(edgeSrc); const em = E.run(E.DEFAULTS); const er = rowOf(E.verify()); check('at the boundary the counts are equal and the "fewer pairs" row says NO', edgeSrc !== src && em.before.inOrder === em.after.inOrder && em.route.takers > 0 && er && er.ok === false && !er.untested, er ? er.detail : 'row not found'); const loose = 'm.after.inOrder < m.before.inOrder,'; const lr = src.includes(loose) ? rowOf(engine(atEdge(src.replace(loose, 'm.after.inOrder <= m.before.inOrder,'))).verify()) : null; check('and this check sees the row loosened to <= (it would say yes there)', Boolean(lr) && lr.ok === true, lr ? String(lr.ok) : 'the mutation did not match'); // True readings in each column, recounted here. Before: number minus // quality is the drawn error. Control: that gap scaled by s. Over a grid. let states = 0; let beforeOk = 0; let controlOk = 0; let agree = 0; for (const seed of [1975, 1970, 3, 99]) { for (const sigma of [0, 0.5, 1.25, 2]) { for (const who of ['bottom', 'anyone']) { for (const share of [0, 0.4, 1]) { for (const boost of [0, 0.5, 2, 4]) { const m = G.run({ seed, sigma, who, share, boost }); states += 1; let b = 0; let c = 0; for (let i = 0; i < 50; i += 1) { if (Math.abs((m.pop.proxy[i] - m.pop.quality[i]) - sigma * m.pop.noise[i]) < 1e-12) b += 1; const gap = m.better.proxy[i] - m.better.quality[i]; if (Math.abs(gap - m.ratio * sigma * m.pop.noise[i]) < 1e-9) c += 1; } if (b === 50) beforeOk += 1; if (c === 50) controlOk += 1; if (b === m.honestBefore && c === m.honestControl) agree += 1; } } } } } check('in every state of a grid, every number before and in the control reads its quality', beforeOk === states && controlOk === states, `${beforeOk} / ${controlOk} of ${states}`); check('and the engine\'s counts are the recounted ones', agree === states, `${agree} of ${states}`); { const from = 'return { quality: stretch(pop.quality, s, anchor),'; const M = src.includes(from) ? engine(src.replace(from, 'return { quality: pop.quality,')) : null; const mm = M && M.run(M.DEFAULTS); check('a control that moves only the number shows in its true-readings count', Boolean(mm) && mm.honestControl < mm.pop.n, mm ? `${mm.honestControl} of ${mm.pop.n}` : 'no match'); } // "Measured low": the takers' mean error, recounted from the arrays. let tStates = 0; let tAgree = 0; let fires = 0; let notLow = 0; for (let seed = 1965; seed < 1975; seed += 1) { for (const sigma of [0.25, 0.5, 1.25]) { for (const who of ['bottom', 'anyone']) { for (const share of [0.3, 0.7]) { for (const boost of [0.5, 1, 2]) { const m = G.run({ seed, sigma, who, share, boost }); const errs = m.pop.noise.filter((_, i) => m.route.took[i]).map((e) => sigma * e); const mean = errs.length ? errs.reduce((t, e) => t + e, 0) / errs.length : null; tStates += 1; if (mean === null ? m.takersError === null : Math.abs(mean - m.takersError) < 1e-12) tAgree += 1; if (m.after.inOrder > m.before.inOrder) { fires += 1; if (mean >= 0) notLow += 1; } } } } } } check('the takers\' mean error is the recounted one', tAgree === tStates, `${tAgree} of ${tStates}`); check('and in states where the route gains pairs, the takers are often not measured low', fires > 0 && notLow > 0, `${notLow} of ${fires}`); const r70 = G.run({ seed: 1970, sigma: 1.25, who: 'anyone', share: 0.7, boost: 0.5 }); check('the red team\'s state gains pairs with takers measured high on average', r70.after.inOrder > r70.before.inOrder && r70.takersError > 0, String(r70.takersError)); // The ledger says "anyone" works like extra measurement error at moderate // boosts. The matched error is sqrt(sigma^2 + b^2 p (1 - p)), the spread of // the error plus a lift of b with probability p. The mean tau after the // route sits near the closed form at that error, and parts from it at 4. const tauAt = (sigma) => (2 / Math.PI) * Math.asin(1 / Math.sqrt(1 + sigma * sigma)); const meanTau = (o) => { let t = 0; for (let seed = 1; seed <= 400; seed += 1) t += G.run({ ...o, seed, who: 'anyone' }).after.tau; return t / 400; }; const gaps = [[0.5, 0.7, 2], [0.5, 0.5, 2], [1, 0.3, 3], [0.5, 0.5, 1]].map(([s, q, b]) => Math.abs(meanTau({ sigma: s, share: q, boost: b }) - tauAt(Math.sqrt(s * s + b * b * q * (1 - q))))); const far = Math.abs(meanTau({ sigma: 0.5, share: 0.5, boost: 4 }) - tauAt(Math.sqrt(0.25 + 16 * 0.25))); check('with "anyone", the route matches extra measurement error at moderate boosts', gaps.every((g) => g < 0.02), gaps.map((g) => g.toFixed(3)).join(' ')); check('and parts from it at boost 4, so the ledger says moderate', far > 0.04, far.toFixed(3)); const ledgerText = spec.ledger.map((e) => e.body).join(' '); check('and the ledger says the model applies no pressure and shows no reason to take the route', /Nothing in it applies pressure either/.test(ledgerText) && /not why they take it/.test(ledgerText)); check('and says the share is a slider and "anyone" acts like measurement error', /The share who take the route is a slider set by hand/.test(ledgerText) && /works like extra measurement error/.test(ledgerText)); } const FLOOR = 90; check(`at least ${FLOOR} checks ran`, p + f >= FLOOR, String(p + f)); console.log(`\ngoodhart: ${p} passed, ${f} failed. Every score is computed a second way and by ` + 'scipy, the closed forms are held against an independent sweep and against the ' + 'tutorial\'s printed values, and every quotation against the archived source.'); process.exit(f ? 1 : 0);