solargod/verify.html
monsterrobotparty c1c1afb0db 🛳 vessel: vendor three@0.170.0 (offline) + deep-time epoch mode (opus)
V.1 — cut the CDN tether. Vendored the exact pinned files the importmap used
(three.module.js r170 + OrbitControls + CSS2DRenderer, ~1.29 MB) under
vendor/three/, mirroring the package layout so the addons' bare `import from
'three'` resolves. Import graph verified shallow (addons import only 'three';
the bundle is self-contained). Both importmaps (index.html, verify.html) now
point at ./vendor/three/… — zero jsdelivr/CDN URLs remain. MIT LICENSE +
PROVENANCE.txt retained.

V.2 — open deep time. EPOCH toggle (1800–2050 / 3000 BC–3000 AD) next to
MEGA/TRUE; deep mode is a hash-carried page mode (&e=1) read before ephem/clock
init → ephem.setExtendedRange(true) + CONFIG.time swaps to the deep bounds.
Timeline end labels + scrub tooltip now read from CONFIG (were hardcoded
1800/2050). Toggle serializes current view and reloads with the flipped e token
(orbit paths + element resolution are baked per-session). Appended 1 decade/s
and 1 century/s rates. Hash e token added to applyHash/serializeHash.

V.3 — appended verify gates (PERIHELION-V): Jupiter @1000 BC (extended 2a+2b)
vs live Horizons barycenter tol 0.15 AU (Δ 0.0041); Mercury @2500 tol 0.02 AU
(Δ 3e-5); Table 2b M-correction proven live (zeroing it worsens Jupiter@1000BC
by 0.0214 AU). Core 13 gates untouched.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-16 10:28:23 +10:00

365 lines
20 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>SOLARGOD ▸ verify</title>
<style>
body { background: #05060a; color: #e6ddca; font-family: ui-monospace, Menlo, monospace; font-size: 13px; padding: 24px; }
h1 { color: #d9a441; font-size: 18px; letter-spacing: 2px; }
table { border-collapse: collapse; width: 100%; max-width: 960px; margin-top: 12px; }
th, td { text-align: left; padding: 5px 10px; border-bottom: 1px solid rgba(217,164,65,0.14); }
th { color: #8a8674; font-weight: 600; text-transform: uppercase; font-size: 10px; }
.pass { color: #6fcf6f; font-weight: 700; }
.fail { color: #e06a5a; font-weight: 700; }
.pending { color: #8a8674; }
.detail { color: #8a8674; font-size: 11px; }
#summary { margin-top: 16px; font-size: 15px; }
.importmap-note { color: #8a8674; }
</style>
<!-- Three.js vendored (offline-first, zero CDN). See vendor/three/PROVENANCE.txt. -->
<script type="importmap">
{ "imports": { "three": "./vendor/three/build/three.module.js", "three/addons/": "./vendor/three/examples/jsm/" } }
</script>
</head>
<body>
<h1>SOLARGOD ▸ VERIFY</h1>
<div class="importmap-note">lib.js + ephem.js + scale.js assertions · live Horizons via serve.py proxy. Serve with <code>python3 serve.py</code> then open <code>/verify.html</code>.</div>
<table><thead><tr><th>#</th><th>gate</th><th>result</th><th>detail</th></tr></thead><tbody id="rows"></tbody></table>
<div id="summary" class="pending">running…</div>
<script type="module">
import * as lib from './js/lib.js';
import * as ephem from './js/ephem.js';
import * as scale from './js/scale.js';
import { CONFIG } from './js/config.js';
scale.init(CONFIG);
const rows = document.getElementById('rows');
let n = 0, passed = 0, failed = 0;
function row(name, ok, detail) {
n++; if (ok) passed++; else failed++;
const tr = document.createElement('tr');
tr.innerHTML = `<td>${n}</td><td>${name}</td><td class="${ok ? 'pass' : 'fail'}">${ok ? 'PASS' : 'FAIL'}</td><td class="detail">${detail}</td>`;
rows.appendChild(tr);
}
function pendingRow(name) {
const tr = document.createElement('tr');
tr.innerHTML = `<td>—</td><td>${name}</td><td class="pending">…</td><td class="detail">querying Horizons…</td>`;
rows.appendChild(tr);
return tr;
}
// ---- 1. Horizons ground truth (brief §9), JD 2461236.5 = 2026-07-15 TDB ----
const JD = 2461236.5;
const truth = {
mars: { x: +1.05351078, y: +1.00862522, z: -0.00469549, tol: 0.005 },
jupiter: { x: -3.01967256, y: +4.33218144, z: +0.04956505, tol: 0.01 },
earth: { x: +0.38381080, y: -0.94118486, z: +0.00005421, tol: 0.003 },
};
for (const [id, t] of Object.entries(truth)) {
const p = ephem.helioEcl(id, JD);
const worst = Math.max(Math.abs(p.x - t.x), Math.abs(p.y - t.y), Math.abs(p.z - t.z));
row(`${id} @ 2026-07-15 (heliocentric ecliptic AU)`, worst <= t.tol,
`Δmax=${worst.toExponential(2)} AU (tol ${t.tol}) · got (${p.x.toFixed(5)}, ${p.y.toFixed(5)}, ${p.z.toFixed(5)})`);
}
// ---- 2. Kepler solver converges for e = 0.97 ----
{
const M = 0.5, e = 0.97, E = ephem.solveKepler(M, e);
const resid = Math.abs(E - e * Math.sin(E) - M);
row('Kepler solver e=0.97', resid < 1e-9, `residual ${resid.toExponential(2)} rad`);
}
// ---- 3. orbitPath endpoints join ----
{
const path = ephem.orbitPath('mercury', JD, 128);
const m = path.length / 3;
const d = Math.max(Math.abs(path[0] - path[(m-1)*3]), Math.abs(path[1] - path[(m-1)*3+1]), Math.abs(path[2] - path[(m-1)*3+2]));
row('orbitPath endpoints join', d < 1e-12, `Δ ${d.toExponential(2)} AU`);
}
// ---- 4. eclToWorld round-trips ----
{
const v = { x: 1.234, y: -5.678, z: 0.910 };
const w = lib.eclToWorld(v, {}); const back = lib.worldToEcl(w, {});
const d = Math.max(Math.abs(back.x - v.x), Math.abs(back.y - v.y), Math.abs(back.z - v.z));
row('eclToWorld round-trip', d === 0, `Δ ${d}`);
}
// ---- 5. MEGA→TRUE preserves direction (unit dot ≈ 1) ----
{
const ecl = ephem.helioEcl('jupiter', JD);
const a = scale.viewFromEcl(ecl, {});
scale.setMode('true');
for (let i = 0; i < 60 && scale.isTransitioning(); i++) scale.update(100);
const b = scale.viewFromEcl(ecl, {});
const na = Math.hypot(a.x, a.y, a.z), nb = Math.hypot(b.x, b.y, b.z);
const dot = (a.x * b.x + a.y * b.y + a.z * b.z) / (na * nb);
row('MEGA→TRUE keeps direction', Math.abs(dot - 1) < 1e-9, `unit dot = ${dot.toFixed(12)}`);
scale.setMode('mega'); for (let i = 0; i < 60 && scale.isTransitioning(); i++) scale.update(100);
}
// ---- 5b. Belt sample propagates as a torus between Mars & Jupiter (Stage 5) ----
{
// three synthetic MBAs (a in 2.23.2) at random anomalies must land, in the
// compressed view, at radii strictly between Mars's and Jupiter's.
const rMars = scale.radial(1.524), rJup = scale.radial(5.203);
const els = [
{ a: 2.3, e: 0.1, i: 5, om: 30, w: 60, ma: 10, epoch: JD },
{ a: 2.77, e: 0.08, i: 10, om: 80, w: 73, ma: 200, epoch: JD },
{ a: 3.15, e: 0.15, i: 15, om: 150, w: 200, ma: 300, epoch: JD },
];
let ok = true, rmin = Infinity, rmax = 0;
for (const el of els) {
const p = ephem.smallBodyEcl(el, JD, {});
const r = scale.radial(Math.hypot(p.x, p.y, p.z));
rmin = Math.min(rmin, r); rmax = Math.max(rmax, r);
if (!(r > rMars && r < rJup)) ok = false;
}
row('main belt sits between Mars & Jupiter (view radii)', ok, `belt view-r ${rmin.toFixed(1)}${rmax.toFixed(1)} ∈ (Mars ${rMars.toFixed(1)}, Jupiter ${rJup.toFixed(1)})`);
}
// ---- 5c. Halley's aphelion reaches beyond Neptune's orbit (Stage 5 gate) ----
{
const a = 17.93, e = 0.9679; // 1P/Halley (SBDB)
const aphelion = a * (1 + e), neptune = 30.07;
row("Halley aphelion beyond Neptune", aphelion > neptune, `aphelion ${aphelion.toFixed(1)} AU > Neptune ${neptune} AU`);
}
// ---- 6+7. Live Horizons via proxy — two extra epochs (also gates the proxy) ----
// Mars @ J2000.0 (JD 2451545.0) and Jupiter @ 1900-01-01 (JD 2415020.5).
async function horizonsVec(command, jd) {
const q = {
format: 'text', COMMAND: `'${command}'`, OBJ_DATA: 'NO', MAKE_EPHEM: 'YES',
EPHEM_TYPE: 'VECTORS', CENTER: "'500@10'", TLIST: String(jd),
REF_PLANE: 'ECLIPTIC', REF_SYSTEM: 'J2000', VEC_TABLE: '1', OUT_UNITS: 'AU-D', CSV_FORMAT: 'NO',
};
const qs = Object.entries(q).map(([k, v]) => `${k}=${encodeURIComponent(v)}`).join('&');
const res = await fetch(`${CONFIG.proxy.horizons}?${qs}`);
const text = await res.text();
const soe = text.indexOf('$$SOE'), eoe = text.indexOf('$$EOE');
if (soe < 0 || eoe < 0) throw new Error('no $$SOE block — ' + text.slice(0, 160));
const block = text.slice(soe, eoe);
const gx = block.match(/X\s*=\s*(-?[\d.]+E?[-+]?\d*)/i);
const gy = block.match(/Y\s*=\s*(-?[\d.]+E?[-+]?\d*)/i);
const gz = block.match(/Z\s*=\s*(-?[\d.]+E?[-+]?\d*)/i);
if (!gx || !gy || !gz) throw new Error('could not parse X/Y/Z');
return { x: parseFloat(gx[1]), y: parseFloat(gy[1]), z: parseFloat(gz[1]) };
}
async function liveCheck(label, id, command, jd, tol) {
const tr = pendingRow(label);
try {
const got = await horizonsVec(command, jd);
const mine = ephem.helioEcl(id, jd);
const worst = Math.max(Math.abs(got.x - mine.x), Math.abs(got.y - mine.y), Math.abs(got.z - mine.z));
tr.remove();
row(label, worst <= tol, `Δmax=${worst.toExponential(2)} AU (tol ${tol}) · Horizons (${got.x.toFixed(5)}, ${got.y.toFixed(5)}, ${got.z.toFixed(5)})`);
} catch (err) {
tr.remove();
row(label, false, `proxy/Horizons error: ${err.message}`);
}
finish();
}
function finish() {
const s = document.getElementById('summary');
s.textContent = `${passed}/${n} gates pass` + (failed ? `${failed} FAILING` : ' — ALL PASS ✦');
s.className = failed ? 'fail' : 'pass';
}
// ---- 10. Voyager 1 (-31) @ 2026-07-15 ≈ (32.07, 136.21, +98.55) AU (brief §9) ----
async function craftCheck(label, command, jd, want, tol) {
const tr = pendingRow(label);
try {
const got = await horizonsVec(command, jd);
const worst = Math.max(Math.abs(got.x - want.x), Math.abs(got.y - want.y), Math.abs(got.z - want.z));
tr.remove();
row(label, worst <= tol, `Δmax=${worst.toFixed(3)} AU (tol ${tol}) · Horizons (${got.x.toFixed(2)}, ${got.y.toFixed(2)}, ${got.z.toFixed(2)}) · ${Math.hypot(got.x,got.y,got.z).toFixed(1)} AU out`);
} catch (err) { tr.remove(); row(label, false, `Horizons error: ${err.message}`); }
finish();
}
// ---- 11. JWST (-170) hugs Earth (Δ < 0.02 AU) ----
async function jwstCheck() {
const label = 'JWST (-170) hugs Earth Δ<0.02 AU';
const tr = pendingRow(label);
try {
const j = await horizonsVec('-170', 2461236.5);
const e = await horizonsVec('399', 2461236.5);
const d = Math.hypot(j.x - e.x, j.y - e.y, j.z - e.z);
tr.remove();
row(label, d < 0.02, `Δ=${(d).toFixed(4)} AU from Earth (${(d*lib.AU_KM/1e6).toFixed(2)}M km)`);
} catch (err) { tr.remove(); row(label, false, `Horizons error: ${err.message}`); }
finish();
}
finish();
// Sequential (not parallel) — Horizons throttles bursts of concurrent requests.
(async () => {
await liveCheck('Mars @ J2000.0 vs live Horizons', 'mars', '499', 2451545.0, 0.01);
await liveCheck('Jupiter @ 1900-01-01 vs live Horizons', 'jupiter', '599', 2415020.5, 0.02);
await craftCheck('Voyager 1 (-31) @ 2026-07-15', '-31', 2461236.5, { x: -32.07, y: -136.21, z: 98.55 }, 0.05);
await jwstCheck();
})();
/* PERIHELION-N — NEARGOD close-approach gate. A real NEO's propagated
heliocentric distance to Earth(≈EMB) at its CAD close-approach epoch must
match CAD's miss distance within 25% — generous on purpose: Earth≈EMB,
two-body propagation, CAD dist is geocentric; the point is catching
wrong-by-an-AU bugs, not arcseconds. Exercises the full lane-N pipeline:
CAD des → sbdb.api full-prec elements → parseSbdbElements → shared Kepler
core (ephem.smallBodyEcl), the identical code path the layer draws. */
import { parseSbdbElements } from './js/layers/neos.js';
async function neoCloseApproachGate() {
const label = 'NEO propagated miss vs CAD (real orbit)';
const tr = pendingRow(label);
try {
const LD_AU = 384400 / lib.AU_KM;
// Fixed window anchored at the sim's default date so the gate is
// deterministic and always populated (past close approaches stay in CAD).
const cad = await (await fetch(`${CONFIG.proxy.cad}?date-min=2026-06-16&date-max=2026-08-15&dist-max=0.05&sort=dist&limit=40`)).json();
const F = cad.fields, ix = (k) => F.indexOf(k);
const rows = cad.data || [];
if (!rows.length) throw new Error('CAD returned no rows');
// Pick the largest-miss approach: 25% of a bigger distance is the most
// robust absolute tolerance (least sensitive to EMB≈Earth + two-body drift).
let best = null;
for (const r of rows) { const dist = +r[ix('dist')]; if (!best || dist > best.dist) best = { des: r[ix('des')], jd: +r[ix('jd')], dist }; }
const j = await (await fetch(`proxy/sbdb_lookup?sstr=${encodeURIComponent(best.des)}&full-prec=true`)).json();
const el = parseSbdbElements(j);
if (!el) throw new Error(`no elliptical elements for ${best.des}`);
const neo = ephem.smallBodyEcl(el, best.jd, {});
const emb = ephem.helioEcl('earth', best.jd, {});
const d = Math.hypot(neo.x - emb.x, neo.y - emb.y, neo.z - emb.z);
const relErr = Math.abs(d - best.dist) / best.dist;
tr.remove();
row(label, relErr < 0.25,
`${best.des} @ jd ${best.jd.toFixed(3)}: propagated ${(d / LD_AU).toFixed(2)} LD vs CAD ${(best.dist / LD_AU).toFixed(2)} LD — err ${(relErr * 100).toFixed(2)}% (tol 25%)`);
} catch (err) { tr.remove(); row(label, false, `neos/SBDB error: ${err.message}`); }
finish();
}
neoCloseApproachGate();
/* PERIHELION-E — sky-events math gates (findEvents, pure math, no network).
Dynamic import keeps the whole lane-E addition one contiguous end block. */
{
const { findEvents, nextEvent } = await import('./js/layers/events.js');
const jdOf = (y, mo, d) => lib.jdFromUnixMs(Date.UTC(y, mo - 1, d));
const dstr = (jd) => lib.formatUTCDate(lib.unixMsFromJd(jd));
const evs = findEvents(jdOf(2026, 1, 1), jdOf(2028, 1, 1));
// (E-a) Mars opposition within ±2 days of the known 2027-02-19 event.
{
const target = jdOf(2027, 2, 19);
const opp = evs.filter((e) => e.bodies[0] === 'mars' && e.type === 'opposition')
.map((e) => ({ jd: e.jd, d: Math.abs(e.jd - target) })).sort((a, b) => a.d - b.d)[0];
row('Mars opposition ±2 d of 2027-02-19', opp && opp.d <= 2,
opp ? `found ${dstr(opp.jd)}, Δ=${opp.d.toFixed(2)} d (tol 2)` : 'no Mars opposition found');
}
// (E-b) Venus greatest elongations: consecutive ones alternate E/W and are
// ~142 d apart (the elongation↔elongation cadence). NB the brief's "~72 d"
// is the elongation↔inferior-conjunction gap, gated separately below.
const vElong = evs.filter((e) => e.bodies[0] === 'venus' && e.type.startsWith('elongation'));
{
const ok = vElong.length >= 2;
const gap = ok ? vElong[1].jd - vElong[0].jd : NaN;
const alt = ok && vElong[0].type !== vElong[1].type; // east then west (or vice versa)
row('Venus consecutive greatest elongations (alternate E/W, 120160 d)',
ok && alt && gap >= 120 && gap <= 160,
ok ? `${dstr(vElong[0].jd)} (${vElong[0].type.split('-')[1]}) → ${dstr(vElong[1].jd)} (${vElong[1].type.split('-')[1]}) = ${gap.toFixed(1)} d` : 'fewer than 2 elongations');
}
// (E-b) The brief's ~72 d: a Venus greatest elongation to the neighbouring
// inferior conjunction — 6080 d — which also gates the conjunction finder.
{
const vInf = evs.filter((e) => e.bodies[0] === 'venus' && e.label.includes('inferior'));
const ok = vElong.length && vInf.length;
const gap = ok ? vInf.map((c) => Math.abs(c.jd - vElong[0].jd)).sort((a, b) => a - b)[0] : NaN;
row('Venus greatest elongation → inferior conjunction 6080 d',
ok && gap >= 60 && gap <= 80, ok ? `gap = ${gap.toFixed(1)} d (tol 6080)` : 'missing elongation/conjunction');
}
// (E-c) nextEvent agrees with the scan: Mars headline is the same opposition.
{
const ne = nextEvent('mars', jdOf(2026, 7, 16));
const target = jdOf(2027, 2, 19);
row('nextEvent(mars) → 2027 opposition', ne && ne.type === 'opposition' && Math.abs(ne.jd - target) <= 2,
ne ? `${dstr(ne.jd)} · ${ne.label}` : 'none');
}
finish();
}
/* PERIHELION-V — deep-time (extended Table 2a/2b) accuracy gates.
Capture the "mine" positions with the extended range ON, then restore it OFF
synchronously and atomically: ES modules are singletons, so setExtendedRange
mutates the same ephem the live gates above (Mars@J2000 / Jupiter@1900, which
expect Table 1) share — the flag must never be ON across an await. */
const JD_JUP_1000BC = 1355818.5; // 1000 BC-01-01
const JD_MERC_2500 = lib.jdFromUnixMs(Date.UTC(2500, 0, 1));
// Scratch extended-Jupiter resolve — a faithful copy of ephem's Table 2a+2b
// math so the 2b M-correction can be ZEROED without editing ephem.js.
const T2A_JUP = { el: [5.20248019, 0.04853590, 1.29861416, 34.33479152, 14.27495244, 100.29282654],
rate: [-0.00002864, 0.00018026, -0.00322699, 3034.90371757, 0.18199196, 0.13024619] };
const B2_JUP = [-0.00012452, 0.06064060, -0.35635438, 38.35125000]; // [b, c, s, f]
function jupiterExtScratch(jd, with2b) {
const T = lib.centuriesSinceJ2000(jd);
const a = T2A_JUP.el[0] + T2A_JUP.rate[0] * T, e = T2A_JUP.el[1] + T2A_JUP.rate[1] * T;
const I = T2A_JUP.el[2] + T2A_JUP.rate[2] * T, L = T2A_JUP.el[3] + T2A_JUP.rate[3] * T;
const wb = T2A_JUP.el[4] + T2A_JUP.rate[4] * T, Om = T2A_JUP.el[5] + T2A_JUP.rate[5] * T;
let M = L - wb;
if (with2b) { const [b, c, s, f] = B2_JUP; M += b * T * T + c * Math.cos(f * T * lib.DEG) + s * Math.sin(f * T * lib.DEG); }
return ephem.eclFromElements(a, e, I, Om, wb - Om, M, {});
}
let jupMine, mercMine, jupZeroed;
{
ephem.setExtendedRange(true);
jupMine = ephem.helioEcl('jupiter', JD_JUP_1000BC, {});
mercMine = ephem.helioEcl('mercury', JD_MERC_2500, {});
ephem.setExtendedRange(false); // restore before any await
jupZeroed = jupiterExtScratch(JD_JUP_1000BC, false); // 2b correction removed
}
const dmax3 = (a, b) => Math.max(Math.abs(a.x - b.x), Math.abs(a.y - b.y), Math.abs(a.z - b.z));
(async () => {
// Wait for the base live gates + lane-N/E gates (18 total) to finish — ThreadingHTTPServer
// does NOT serialize upstream calls, so we must never fetch Horizons in parallel.
for (let i = 0; i < 480 && n < 18; i++) await new Promise((r) => setTimeout(r, 250));
// Jupiter ground truth: barycenter '5' (DE441, long span). Planet-center '599'
// has NO Horizons ephemeris before A.D. 1600; the SSD approximate elements model
// the giant-planet SYSTEM barycenter anyway (Jupiter↔bary ≈ 5e-4 AU ≪ tol).
let jupH = null;
{
const label = 'Jupiter @1000 BC (extended 2a+2b) vs live Horizons — tol 0.15 AU';
const tr = pendingRow(label);
try {
jupH = await horizonsVec('5', JD_JUP_1000BC);
const w = dmax3(jupMine, jupH);
tr.remove();
row(label, w <= 0.15, `Δmax=${w.toFixed(4)} AU (tol 0.15) · bary '5' (${jupH.x.toFixed(4)}, ${jupH.y.toFixed(4)}, ${jupH.z.toFixed(4)}) · mine (${jupMine.x.toFixed(4)}, ${jupMine.y.toFixed(4)}, ${jupMine.z.toFixed(4)})`);
} catch (err) { tr.remove(); row(label, false, `Horizons error: ${err.message}`); }
finish();
}
{
const label = 'Mercury @2500-01-01 (extended 2a) vs live Horizons — tol 0.02 AU';
const tr = pendingRow(label);
try {
const mercH = await horizonsVec('199', JD_MERC_2500);
const w = dmax3(mercMine, mercH);
tr.remove();
row(label, w <= 0.02, `Δmax=${w.toExponential(2)} AU (tol 0.02) · Horizons (${mercH.x.toFixed(5)}, ${mercH.y.toFixed(5)}, ${mercH.z.toFixed(5)})`);
} catch (err) { tr.remove(); row(label, false, `Horizons error: ${err.message}`); }
finish();
}
{
// Prove Table 2b's M-correction is LIVE: zeroing it must worsen Jupiter@1000 BC
// (reuses the Jupiter Horizons vector fetched above — no extra request).
const label = 'Table 2b M-correction engages — zeroing it worsens Jupiter@1000 BC';
if (jupH) {
const dWith = dmax3(jupMine, jupH), dZero = dmax3(jupZeroed, jupH);
row(label, dZero > dWith, `with 2b Δ=${dWith.toFixed(4)} AU → zeroed Δ=${dZero.toFixed(4)} AU (worse by ${(dZero - dWith).toFixed(4)} AU)`);
} else {
row(label, false, 'skipped — Jupiter Horizons fetch failed above');
}
finish();
}
})();
</script>
</body>
</html>