From da016d8b1d5350a8fe1b8103ee215fe9df075716 Mon Sep 17 00:00:00 2001 From: doxa-maps-88655c70 <88627970+doxa-maps-88655c70@users.noreply.github.com> Date: Fri, 25 Sep 2026 18:40:36 +0000 Subject: [PATCH] The tree-identity guard can check the DATA a gate judges, not just the bundles MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit servesThisTree fingerprints manifest.json, which describes BUNDLES. MEASURED 2026-09-25 (gospel): verify:gospel-map adopted another worktree's server and reported PASS, reading 16,867 links, 93 short of this tree — the manifests were byte-identical because the change under test was a data asset (doxa-gospel-access.json); a planted jw.org link went undetected. - servesThisTree(base, repo, { assets }) / requireThisTree(base, repo, label, { assets }): a gate declares the tree-relative data files it judges, and their served bytes must equal disk too. A mismatch or a missing file is a DIFFERENT build -> requireThisTree exits 75 NOT MEASURED, naming the file. - verify-gospel-map converges onto it (its per-gate copy of the check is gone): the tables are declared in its adopt check, so a same-manifest/different-data server is not adopted and its own server is started instead; a server you NAMED that is another tree is now NOT MEASURED (75), not "SKIP — nothing served" (exit 0). Negative-tested: a copy of this tree with an identical manifest and one planted link in doxa-gospel-access.json — without assets the guard says "same build" (the incident); with assets it refuses, naming the file; requireThisTree exits 75; this tree passes; verify:gospel-map named at the copy exits 75. --- internal/_serves-this-tree.mjs | 28 +++++++++++++++++++---- internal/verify-gospel-map.mjs | 42 +++++++++++++++++++--------------- 2 files changed, 47 insertions(+), 23 deletions(-) diff --git a/internal/_serves-this-tree.mjs b/internal/_serves-this-tree.mjs index 8d4bc86e1..897bb7967 100644 --- a/internal/_serves-this-tree.mjs +++ b/internal/_serves-this-tree.mjs @@ -29,9 +29,11 @@ const MANIFEST = `${TREE_NAME}/manifest.json` /** * @param {string} base e.g. http://127.0.0.1:4173 (a trailing /doxa-maps/ is tolerated) * @param {string} repo absolute path to the repo root + * @param {{assets?: string[]}} [opts] data files this gate JUDGES (tree-relative, e.g. + * 'assets/tables/doxa-gospel-access.json'); their served bytes must equal disk too * @returns {Promise<{ok:boolean, why:string}>} */ -export async function servesThisTree(base, repo) { +export async function servesThisTree(base, repo, { assets = [] } = {}) { let local try { local = createHash('sha256').update(readFileSync(resolve(repo, 'public', MANIFEST))).digest('hex') @@ -45,7 +47,25 @@ export async function servesThisTree(base, repo) { if (served !== local) { return { ok: false, why: `${root} serves a DIFFERENT build than public/ — another worktree, or a server that needs restarting` } } - return { ok: true, why: 'same build' } + // ★ THE MANIFEST DESCRIBES BUNDLES, NOT DATA. MEASURED 2026-09-25 (gospel): verify:gospel-map adopted + // another worktree's server on :4173 and reported PASS — the three worktrees' manifests were + // byte-identical because the change under test was a DATA asset (doxa-gospel-access.json), which the + // manifest never mentions; a planted jw.org link went undetected. So a gate whose subject is a data + // file DECLARES it (paths relative to the tree, e.g. 'assets/tables/doxa-gospel-access.json') and its + // served bytes are compared with disk too. Any difference is a different tree for THIS gate. + for (const rel of assets) { + let disk + try { disk = readFileSync(resolve(repo, 'public', TREE_NAME, rel)) } catch { + return { ok: false, why: `no local public/${TREE_NAME}/${rel} to compare against — build the tree first` } + } + const a = await fetch(`${root}/${TREE_NAME}/${rel}`).catch(() => null) + if (!a || !a.ok) return { ok: false, why: `${root} serves a DIFFERENT build — ${rel} is not served there (${a ? a.status : 'no answer'})` } + const got = Buffer.from(await a.arrayBuffer()) + if (!got.equals(disk)) { + return { ok: false, why: `${root} serves a DIFFERENT build — the manifest matches but ${rel} does not (another worktree's data, or a stale server)` } + } + } + return { ok: true, why: assets.length ? `same build, and ${assets.length} declared data file(s) byte-identical` : 'same build' } } @@ -100,7 +120,7 @@ export async function serveThisTree(dir) { * @param {string} repo absolute path to the repo root * @param {string} label the gate's own name, for the message */ -export async function requireThisTree(base, repo, label) { +export async function requireThisTree(base, repo, label, { assets = [] } = {}) { let origin = String(base || '') // A gate that loads file:// URLs is reading THIS checkout by construction — there is no server to // be wrong about, so the guard stays silent rather than inventing a precondition. @@ -116,7 +136,7 @@ export async function requireThisTree(base, repo, label) { if (!/^(localhost|127\.|0\.0\.0\.0$|\[?::1\]?$)/.test(host)) return { ok: true, why: 'remote target, not a worktree port' } } catch { /* unparseable — fall through to the content check */ } try { origin = new URL(origin).origin } catch { /* already an origin, or unusable — pass through */ } - const verdict = await servesThisTree(origin, repo) + const verdict = await servesThisTree(origin, repo, { assets }) if (/DIFFERENT build/.test(verdict.why)) { console.log(`${label}: NOT MEASURED — ${verdict.why}`) console.log(' Point this gate at a server for THIS checkout (bun run serve) and re-run.') diff --git a/internal/verify-gospel-map.mjs b/internal/verify-gospel-map.mjs index 4149fb906..dea46da98 100644 --- a/internal/verify-gospel-map.mjs +++ b/internal/verify-gospel-map.mjs @@ -49,6 +49,8 @@ import { resolve, dirname } from 'node:path' import { fileURLToPath } from 'node:url' import { TREE_NAME } from './_tree-name.mjs' +// The data files this gate judges; declared to servesThisTree so their served bytes must equal disk. +const GOSPEL_DATA = ['assets/tables/people-group-pins.json', 'assets/tables/doxa-gospel-access.json'] const BASE = (process.env.BASE_URL || process.argv[2] || 'http://127.0.0.1:4173').replace(/\/+$/, '') const GL = ['--use-gl=swiftshader', '--enable-unsafe-swiftshader', '--ignore-gpu-blocklist', '--enable-webgl', '--no-sandbox'] // The screened publishers. Anything else is a doctrine failure, not a typo. @@ -79,13 +81,22 @@ let server = null const alive = async (b) => { const r = await fetch(`${b}/${TREE_NAME}/gospel-resources/resources-map/index.html`).catch(() => null) if (!(r && r.ok)) return false - const tree = await servesThisTree(b, REPO) + // The tables this gate judges are declared, so a server whose manifest matches but whose DATA does + // not (another worktree) is not adopted — the gate starts its own server for THIS checkout instead. + const tree = await servesThisTree(b, REPO, { assets: GOSPEL_DATA }) if (tree.ok) return true console.log(`GOSPEL-MAP: not adopting ${b} — ${tree.why}`) - return false + return 'different' // reachable, but not THIS tree — distinct from "nothing there" } -if (!(await alive(base))) { +const adopted = await alive(base) +if (adopted === 'different' && EXPLICIT) { + // You NAMED this server and it is another tree: that is NOT MEASURED, never a skip (exit 0 read as + // green while the gate had measured nothing of this checkout). + console.log(`GOSPEL-MAP: NOT MEASURED — ${base} is not serving THIS checkout (see above).`) + process.exit(75) +} +if (adopted !== true) { if (EXPLICIT) { console.log(`GOSPEL-MAP: SKIP — nothing served at ${base} (you named it explicitly, so none was started).`) process.exit(0) @@ -175,22 +186,15 @@ try { * The general weakness belongs to _serves-this-tree.mjs and every gate that relies on it; * this only closes it for the two files this gate actually measures. */ - const sameAsDisk = async (rel) => { - const res = await fetch(`${base}/${TREE_NAME}/${rel}`) - if (!res.ok) return `not served (${res.status})` - const served = await res.text() - const disk = readFileSync(resolve(REPO, `public/${TREE_NAME}/${rel}`), 'utf8') - return served === disk ? null : 'differs from this checkout — the server is another tree' - } - for (const rel of ['assets/tables/people-group-pins.json', 'assets/tables/doxa-gospel-access.json']) { - const why = await sameAsDisk(rel) - if (why) { - console.error(`GOSPEL-MAP: NOT MEASURED — ${rel} ${why}.`) - console.error(` Serving ${base}. Point BASE_URL at a server for THIS checkout, or stop the`) - console.error(' one holding the port; a PASS here would be about somebody else\'s data.') - if (server) server.kill() - process.exit(2) - } + // (The per-gate copy of this check that used to live here is now the shared helper's `assets` + // option — the weakness was in _serves-this-tree.mjs, so the fix is there, for every gate.) + const ident = await servesThisTree(base, REPO, { assets: GOSPEL_DATA }) + if (!ident.ok) { + console.error(`GOSPEL-MAP: NOT MEASURED — ${ident.why}.`) + console.error(` Serving ${base}. Point BASE_URL at a server for THIS checkout, or stop the`) + console.error(' one holding the port; a PASS here would be about somebody else\'s data.') + if (server) server.kill() + process.exit(75) } const idxRes = await fetch(`${base}/${TREE_NAME}/assets/tables/people-group-pins.json`)