The tree-identity guard can check the DATA a gate judges, not just the bundles
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.
This commit is contained in:
parent
405db45f19
commit
da016d8b1d
2 changed files with 47 additions and 23 deletions
|
|
@ -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.')
|
||||
|
|
|
|||
|
|
@ -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`)
|
||||
|
|
|
|||
Loading…
Reference in a new issue