Gate the agreement between the surfaces, after finding the same drift three times
Three defects this flight were the same defect, and none of them threw. Every page rendered; only comparing two surfaces caught them: the country sheet listed 36 groups for Indonesia while the map drew 669 pins, because the sheet read a curated subset and the map read the pins; the resource table silently dropped 506 real resources across 377 languages, because its generator skipped any code no pin speaks — in a table titled "every link"; resources attached to `und` told 14 people groups there was Scripture in a language the record explicitly does not identify. A fourth is a matter of time, so the comparison becomes a gate rather than a habit. SIX CLAUSES, all static, 60ms, no browser and no server: no ISO reserved code carries a resource; every resourced code has a language name; the table's reach columns match an independent recount off the pins; every resourced code appears in the table; no table URL exists that the pin index does not hold; and each pin's tier agrees with its code's URLs. Each one is a defect that was actually shipped, not a hypothetical. ★ IT REFUSES TO PASS VACUOUSLY. Five of six clauses assert a NEGATIVE, which is exactly the shape that reads green against empty or mis-keyed input, so an empty corpus is a FAIL rather than a clean run, and a missing input SKIPS loudly with the command to fix it. ★ AND IT SELF-TESTS. `--poison` injects one defect per clause in memory and requires all six to fail. Clause D is included deliberately: it is the one that would have caught the 506 dropped resources, and the first version of this self-test left it unproven — the most valuable clause was the one clause never shown to fail. 6/6 now. Registered in package.json and in build:unlocked, because a gate nothing runs is not a gate — this repo has carded that twice. check:no-orphan-gates went from FAIL to PASS on registration, which is the check confirming itself. The CLI doc quotes the build chain verbatim, so it was updated to match: package.json is what runs, the doc is the copy. No product file changed, so nothing is rebuilt. check:quoted-scripts, check:doc-scripts, check:no-orphan-gates and check:mjs-parse all pass.
This commit is contained in:
parent
117467f7e7
commit
c2046be7ec
3 changed files with 140 additions and 2 deletions
|
|
@ -25,7 +25,7 @@ The scripts these commands are grounded in (from this branch's `package.json`):
|
|||
// claiming to be ground truth, and this one had silently stopped being it.
|
||||
"dev": "bun x vite",
|
||||
"build": "bash internal/build-lock.sh",
|
||||
"build:unlocked": "bun run check:template-literal && bun run check:mjs-parse && bun run check:sfc-parse && bun run check:tab-radius && bun run check:synced-css && bun run check:wp-embed && bun run check:doc-scripts && bun run library-index && bun run check:library-index && bun run clean && bun run token && for d in plugin-designs/*/; do BUNDLE=$(basename \"$d\") bun x vite build || exit 1; done && bun run tree && bun run check:drop && bun run check:parse && bun run verify && bun run check:sri && bun run check:public-hygiene && bun run check:label-joins && bun run check:facet-cascade && bun run check:facet-one-field && bun run check:heartbeat && bun run check:kpi-unit && bun run check:composite-reg && bun run check:descriptions && bun run check:no-abs-paths && bun run check:no-codename && bun run check:dep-names && bun run check:composite-srcs && bun run check:embed-targets && bun run check:nested-token && bun run gallery",
|
||||
"build:unlocked": "bun run check:template-literal && bun run check:mjs-parse && bun run check:coverage-consistent && bun run check:sfc-parse && bun run check:tab-radius && bun run check:synced-css && bun run check:wp-embed && bun run check:doc-scripts && bun run library-index && bun run check:library-index && bun run clean && bun run token && for d in plugin-designs/*/; do BUNDLE=$(basename \"$d\") bun x vite build || exit 1; done && bun run tree && bun run check:drop && bun run check:parse && bun run verify && bun run check:sri && bun run check:public-hygiene && bun run check:label-joins && bun run check:facet-cascade && bun run check:facet-one-field && bun run check:heartbeat && bun run check:kpi-unit && bun run check:composite-reg && bun run check:descriptions && bun run check:no-abs-paths && bun run check:no-codename && bun run check:dep-names && bun run check:composite-srcs && bun run check:embed-targets && bun run check:nested-token && bun run gallery",
|
||||
"preview": "bun x vite preview",
|
||||
"serve": "node internal/serve-strict.mjs", // 404s a directory, like production
|
||||
"cap:sync": "./node_modules/.bin/cap sync android",
|
||||
|
|
|
|||
137
internal/check-gospel-coverage-consistent.mjs
Normal file
137
internal/check-gospel-coverage-consistent.mjs
Normal file
|
|
@ -0,0 +1,137 @@
|
|||
#!/usr/bin/env node
|
||||
/**
|
||||
* check-gospel-coverage-consistent.mjs — the gospel-resources surfaces must agree.
|
||||
*
|
||||
* WHY THIS EXISTS. The same defect was found by hand THREE times in one flight, and each
|
||||
* time it was invisible until someone compared two surfaces:
|
||||
* 1. the country sheet listed 36 groups for Indonesia while the map drew 669 pins there,
|
||||
* because the sheet read a curated 2,106-record subset and the map read the pins;
|
||||
* 2. the resource table silently dropped 506 real resources across 377 languages,
|
||||
* because its generator skipped any code no pin speaks — in a table titled
|
||||
* "every link";
|
||||
* 3. resources attached to `und` ("Undetermined") told 14 people groups there was
|
||||
* Scripture in a language the record explicitly does not identify.
|
||||
* None of these threw. Every page rendered. Only a comparison caught them, so the
|
||||
* comparison is now a gate rather than a habit.
|
||||
*
|
||||
* WHAT IT ASSERTS, all static — no browser, no server, runs in well under a second:
|
||||
* A. no ISO 639-3 special/reserved code carries a resource (und/mul/mis/zxx/qaa-qtz);
|
||||
* B. every code carrying a resource has a language NAME — an unnamed code renders as a
|
||||
* bare ISO in the table and tells a reader nothing;
|
||||
* C. the resource table's reach columns match an independent recount off the pins;
|
||||
* D. the table covers EVERY code that has a resource — the "every link" claim;
|
||||
* E. every resource_url in the table exists in the pin index, so the table cannot
|
||||
* invent a link the map does not hold;
|
||||
* F. each pin's tier agrees with the URLs its code actually carries.
|
||||
*
|
||||
* ★ SELF-TEST: run with --poison to inject each defect in memory and confirm the clause
|
||||
* catches it. A check that has never been shown to fail proves nothing, and five of the
|
||||
* six clauses here are asserting a NEGATIVE, which is exactly the shape that passes
|
||||
* vacuously when the data it reads is empty or mis-keyed.
|
||||
*/
|
||||
import { readFileSync } from 'node:fs'
|
||||
import { resolve, dirname } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
|
||||
const REPO = resolve(dirname(fileURLToPath(import.meta.url)), '..')
|
||||
const POISON = process.argv.includes('--poison')
|
||||
|
||||
const read = (p) => JSON.parse(readFileSync(resolve(REPO, p), 'utf8'))
|
||||
let pins, table
|
||||
try {
|
||||
pins = read('data/tables/people-group-pins.json')
|
||||
table = read('data/tables/gospel-resource-links.json')
|
||||
} catch (e) {
|
||||
console.error(`COVERAGE-CONSISTENT: SKIP — inputs missing (${e.message}).`)
|
||||
console.error(' Run: node plugin-designs/gospel-resources/scripts/build-people-group-pins.mjs')
|
||||
process.exit(0)
|
||||
}
|
||||
|
||||
const codes = { ...pins.codes }
|
||||
let rows = pins.pins
|
||||
let trows = table.rows
|
||||
|
||||
// A gate that reads an empty corpus passes every negative clause while measuring nothing.
|
||||
if (!rows?.length || !trows?.length || !Object.keys(codes).length) {
|
||||
console.error('COVERAGE-CONSISTENT: FAIL — an input is empty; every clause below would pass vacuously.')
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
if (POISON) {
|
||||
// one injected defect per clause, in memory only
|
||||
codes.und = { l: 'Undetermined', d: [{ s: 'Open Bible Stories', u: 'https://example.invalid/und' }] } // A
|
||||
codes.zzq = { l: '', s: 'http://ebible.org/zzq/' } // B
|
||||
trows = [{ ...trows[0], people_groups: (trows[0].people_groups || 0) + 999 }, // C
|
||||
...trows.slice(1, -1),
|
||||
{ ...trows[trows.length - 1], resource_url: 'https://example.invalid/invented' }] // E
|
||||
rows = [[...rows[0].slice(0, 6), 2], ...rows.slice(1)] // F (forced tier 2)
|
||||
}
|
||||
|
||||
const SPECIAL = (c) => c === 'und' || c === 'mul' || c === 'mis' || c === 'zxx' || (c >= 'qaa' && c <= 'qtz')
|
||||
const hasRes = (v) => !!(v && (v.s || v.f || (v.d && v.d.length)))
|
||||
const fails = []
|
||||
const add = (clause, msg) => fails.push(` [${clause}] ${msg}`)
|
||||
|
||||
// A — no resource on a code that is not a language
|
||||
for (const [c, v] of Object.entries(codes)) {
|
||||
if (SPECIAL(c) && hasRes(v)) add('A', `${c} is an ISO reserved code and carries a resource`)
|
||||
}
|
||||
// B — a code with a resource must be named
|
||||
for (const [c, v] of Object.entries(codes)) {
|
||||
if (hasRes(v) && !String(v.l || '').trim()) add('B', `${c} carries a resource but has no language name`)
|
||||
}
|
||||
// C — reach columns recounted off the pins
|
||||
const groups = {}, speakers = {}
|
||||
for (const r of rows) { groups[r[4]] = (groups[r[4]] || 0) + 1; speakers[r[4]] = (speakers[r[4]] || 0) + (r[5] || 0) }
|
||||
let cBad = 0
|
||||
for (const t of trows) {
|
||||
if ((groups[t.iso639_3] || 0) !== t.people_groups) cBad++
|
||||
else if ((speakers[t.iso639_3] || 0) !== t.speakers) cBad++
|
||||
}
|
||||
if (cBad) add('C', `${cBad} table row(s) disagree with a pin recount of people_groups/speakers`)
|
||||
// D — every code with a resource is represented in the table
|
||||
const inTable = new Set(trows.map((t) => t.iso639_3))
|
||||
const missing = Object.keys(codes).filter((c) => hasRes(codes[c]) && !inTable.has(c))
|
||||
if (missing.length) add('D', `${missing.length} code(s) carry a resource but appear in no table row (e.g. ${missing.slice(0, 3).join(', ')})`)
|
||||
// E — the table cannot invent a URL
|
||||
const known = new Set()
|
||||
for (const v of Object.values(codes)) {
|
||||
if (v.s) known.add(v.s); if (v.f) known.add(v.f)
|
||||
for (const d of v.d || []) known.add(d.u)
|
||||
}
|
||||
const invented = trows.filter((t) => t.resource_url && !known.has(t.resource_url))
|
||||
if (invented.length) add('E', `${invented.length} table URL(s) exist in no pin record (e.g. ${invented[0].resource_url})`)
|
||||
// F — tier agrees with the URLs the code holds
|
||||
let fBad = 0
|
||||
for (const r of rows) {
|
||||
const v = codes[r[4]] || {}
|
||||
const want = v.s && v.f ? 2 : (v.s || v.f || (v.d && v.d.length)) ? 1 : 0
|
||||
if (want !== r[6]) fBad++
|
||||
}
|
||||
if (fBad) add('F', `${fBad} pin(s) carry a tier that disagrees with their code's URLs`)
|
||||
|
||||
const clauses = new Set(fails.map((f) => f.slice(3, 4)))
|
||||
if (POISON) {
|
||||
// D is included deliberately: it is the clause that would have caught the 506
|
||||
// resources dropped from a table titled "every link", and the most valuable clause is
|
||||
// the worst one to leave unproven. The injected codes above (und, zzq) are absent from
|
||||
// the table, so they poison D as well as A and B.
|
||||
const want = ['A', 'B', 'C', 'D', 'E', 'F']
|
||||
const caught = want.filter((c) => clauses.has(c))
|
||||
console.log(`COVERAGE-CONSISTENT --poison: ${caught.length}/${want.length} clause(s) caught their injected defect (${caught.join(',')})`)
|
||||
if (caught.length !== want.length) {
|
||||
console.error(` MISSED: ${want.filter((c) => !clauses.has(c)).join(',')} — those clauses cannot fail and prove nothing.`)
|
||||
process.exit(1)
|
||||
}
|
||||
console.log('COVERAGE-CONSISTENT: SELF-TEST PASS — every poisoned clause fails as designed.')
|
||||
process.exit(0)
|
||||
}
|
||||
|
||||
if (fails.length) {
|
||||
console.error(`COVERAGE-CONSISTENT: FAIL — ${fails.length} problem(s):`)
|
||||
for (const f of fails.slice(0, 20)) console.error(f)
|
||||
process.exit(1)
|
||||
}
|
||||
console.log(`COVERAGE-CONSISTENT: PASS — ${rows.length} pins, ${Object.keys(codes).length} codes, ${trows.length} table rows agree.`)
|
||||
console.log(' No reserved code carries a resource; every resourced code is named; reach columns,')
|
||||
console.log(' table coverage, URL provenance and pin tiers all reconcile against the pin index.')
|
||||
|
|
@ -54,7 +54,7 @@
|
|||
"check:facet-changes-map:self-test": "node internal/check-facet-changes-map.mjs --self-test # reverts the fix IN THE LIVE PAGE and requires the gate to FAIL. SELF_TEST_MODE=colour (default, isolates the colour assertion — the reported bug) | props | both | frozen (blocks the source update, isolating the set assertion). All four modes verified caught.",
|
||||
"check:heartbeat": "node internal/check-heartbeat-threshold.mjs # the HEARTBEAT ZONE threshold (the choropleth's top bin) must come from config AND the shipped config must still render today's ramp. Two sides: (B) a configured threshold must MOVE the edge — catches re-hard-coding; (A2) dashboard.config.js's own map.* must still resolve to 5/20/60/150/Infinity — catches the shipped default silently changing. A2 was MISSING at first and heartbeatThreshold:999 shipped past this gate printing 'default ramp unchanged'. A grep for '150' cannot do either job: it false-positives on the legitimate default and false-negatives on a re-hard-coding at 200. run check:heartbeat:self-test for the NEGATIVE tests (3 cases).",
|
||||
"check:heartbeat:self-test": "node internal/check-heartbeat-threshold.mjs --self-test # THREE regression cases, each required to fire ITS OWN assertion: [literal] resolver replaced by the old hard-coded array -> B; [shipped-default] dashboard.config.js heartbeatThreshold changed -> A2; [shipped-mode] heartbeatMode flipped to quantile -> A2. Poisons IN MEMORY, never the file. Testing that the FEATURE works is not testing that the REGRESSION is caught — this gate shipped with only the first and missed the second.",
|
||||
"build:unlocked": "bun run check:template-literal && bun run check:mjs-parse && bun run check:sfc-parse && bun run check:tab-radius && bun run check:synced-css && bun run check:wp-embed && bun run check:doc-scripts && bun run library-index && bun run check:library-index && bun run clean && bun run token && for d in plugin-designs/*/; do BUNDLE=$(basename \"$d\") bun x vite build || exit 1; done && bun run tree && bun run check:drop && bun run check:parse && bun run verify && bun run check:sri && bun run check:public-hygiene && bun run check:label-joins && bun run check:facet-cascade && bun run check:facet-one-field && bun run check:heartbeat && bun run check:kpi-unit && bun run check:composite-reg && bun run check:descriptions && bun run check:no-abs-paths && bun run check:no-codename && bun run check:dep-names && bun run check:composite-srcs && bun run check:embed-targets && bun run check:nested-token && bun run gallery",
|
||||
"build:unlocked": "bun run check:template-literal && bun run check:mjs-parse && bun run check:coverage-consistent && bun run check:sfc-parse && bun run check:tab-radius && bun run check:synced-css && bun run check:wp-embed && bun run check:doc-scripts && bun run library-index && bun run check:library-index && bun run clean && bun run token && for d in plugin-designs/*/; do BUNDLE=$(basename \"$d\") bun x vite build || exit 1; done && bun run tree && bun run check:drop && bun run check:parse && bun run verify && bun run check:sri && bun run check:public-hygiene && bun run check:label-joins && bun run check:facet-cascade && bun run check:facet-one-field && bun run check:heartbeat && bun run check:kpi-unit && bun run check:composite-reg && bun run check:descriptions && bun run check:no-abs-paths && bun run check:no-codename && bun run check:dep-names && bun run check:composite-srcs && bun run check:embed-targets && bun run check:nested-token && bun run gallery",
|
||||
"check:template-literal": "node internal/template-literal-check.mjs",
|
||||
"verify:map-paint": "node internal/verify-map-paint.mjs",
|
||||
"check:sri": "node internal/sri-consistency-check.mjs",
|
||||
|
|
@ -121,6 +121,7 @@
|
|||
"check:spa-tab-order": "node internal/check-spa-tab-order.mjs",
|
||||
"check:backlog-status": "node internal/check-backlog-status-enum.mjs",
|
||||
"check:mjs-parse": "node internal/check-mjs-parses.mjs",
|
||||
"check:coverage-consistent": "node internal/check-gospel-coverage-consistent.mjs # the gospel-resources surfaces must agree: no resource on an ISO RESERVED code (und/mul/mis/zxx), every resourced code NAMED, table reach recounted off the pins, no invented URL, tiers reconcile. Static, sub-second. Self-test: --poison, 6/6 clauses must fail. Written after the same drift defect was found by hand three times.",
|
||||
"check:synced-css:self-test": "node internal/check-synced-css-copies.mjs --self-test",
|
||||
"serve:permissive": "python3 -m http.server 4173 --bind 127.0.0.1 --directory public # LOOPBACK ONLY. This was --bind 0.0.0.0, which published the built tree on every interface of whatever machine ran a gate; measured on the build VPS it answered HTTP 200 on the box's PUBLIC IP while a browser gate was running. Every consumer (verify-browser-all, check:a11y-button-names, check:nav-reachable, smoke:share-all) already defaults to 127.0.0.1, so nothing needed the wider bind. Override with BASE_URL/BASE if you deliberately want to reach it from another host.",
|
||||
"check:doc-scripts": "node internal/check-doc-quotes-scripts.mjs",
|
||||
|
|
|
|||
Loading…
Reference in a new issue