doxa-maps/internal/check-surfaces-read-the-tier.mjs
Doxa Maps e19697673d
Some checks are pending
Deploy to GitHub Pages / build (push) Waiting to run
Deploy to GitHub Pages / deploy (push) Blocked by required conditions
Doxa Maps — the map bundler, 7 production plugins and the Experimental shelf
2026-09-25 20:07:59 +00:00

140 lines
6.9 KiB
JavaScript

/**
* check-surfaces-read-the-tier.mjs — one place decides "this group has nothing".
*
* THE BUG THIS EXISTS FOR, THREE TIMES OVER. The pin index computes a tier per group:
* 0 means no resource was found in any pool checked. The charts profile used to decide the
* same thing again, by listing the resource fields by hand:
*
* none: !c.s && !c.f && !c.au && !c.bv && !c.z && !(c.d && c.d.length)
*
* Every time a source was added, that list was one field short and the two views of the same
* dataset disagreed — 7 codes the first time, then 28 when Scripture Earth landed: drawn as
* having nothing by the chart while the map offered them a working link. Nothing failed. The
* chart simply told a quieter, wronger story than the map, and only a hand count found it.
*
* THE RULE. A surface may READ the tier the pin index computed. It may not RE-DERIVE it from
* the raw resource fields. Adding a source should reach every view for free.
*
* Run with --poison to prove the check can fail: it re-injects the hand-rolled formula into
* a copy of each surface and requires every one to be caught.
*/
import { readFileSync, readdirSync } from 'node:fs'
import { resolve, dirname, basename } from 'node:path'
import { fileURLToPath } from 'node:url'
const HERE = dirname(fileURLToPath(import.meta.url))
const DIR = resolve(HERE, '../plugin-designs/gospel-resources')
const POISON = process.argv.includes('--poison')
/**
* A hand-rolled "has nothing" decision: a negated resource field, joined by && to at least
* two more negated field reads. Matching the SHAPE rather than one spelling is the point —
* the next drift will use a different field list, not this one.
*/
const FIELD = String.raw`!\s*c\.(?:s|f|au|bv|se|ga|z|gp|d)\b[^\n]{0,40}`
const HANDROLLED = new RegExp(`${FIELD}&&\\s*${FIELD}&&\\s*${FIELD}`)
/**
* THE SECOND SHAPE, and the one that got through. The first pattern only caught NEGATED c.*
* fields joined by &&. The country sheet wrote the positive form over the MAPPED row —
* `const hasAny = (r) => !!(r.scripture_url || r.film_url || r.audio || ...)` — so neither
* the keyword filter nor the field pattern saw it. It was stale by 729 groups and 77.9
* million people before this caught it.
*
* WHY THIS IS A NAME RULE AND NOT A CLEVERER PATTERN. Two broader patterns were tried first
* and BOTH produced confident false findings on honest code: "three OR-ed resource fields"
* flags canHear(), which ORs media precisely because it must EXCLUDE text; "spans text and
* media" flags `!!c.s || !!(c.au && /text/…)`, because c.au is text OR audio depending on a
* qualifier, and a `|| ''` default inflates the operator count. A detector that cannot see
* how the expression is constructed reports the wrong thing confidently — the same failure
* this repo has now met in a forced token, a swallowed flag and a class extractor.
*
* So this asks one narrow question it can actually answer: a predicate NAMED for "has
* anything" is deciding the tier by definition, and must read it. Narrow and true beats
* broad and wrong.
*/
const ANYTHING_PREDICATE = /\b(?:hasAny|hasAnything|hasResources|hasRes|anyResource)\b/
function offendersIn(src, label) {
const isScript = label.startsWith('scripts/')
const out = []
const lines = src.split('\n')
for (let i = 0; i < lines.length; i++) {
const line = lines[i]
/* Only lines that are actually deciding emptiness, not e.g. a render guard. */
const namesEmptiness = /\bnone\b|\bgap\b|\bnothing\b|hasNothing/.test(line)
if (namesEmptiness && HANDROLLED.test(line)) {
out.push(`${label}:${i + 1} ${line.trim().slice(0, 90)}`)
continue
}
/* A predicate named for "has anything" IS the tier question; it must read the tier.
Only its DEFINITION is judged — call sites mention the name and decide nothing — and
`_tier` counts, since a mapped row carries it under that name. */
const defines = new RegExp(`(?:const|let|function)\\s+${ANYTHING_PREDICATE.source}`).test(line)
if (!defines) continue
if (isScript) {
/* A build script COMPUTES the tier, so it cannot read one. What it must do instead is
name every pool: the drift here is always a field list that never learned about the
newest source. */
const missing = ['se', 'ga'].filter((f) => !new RegExp(`\\.${f}\\b`).test(line))
if (missing.length) {
out.push(`${label}:${i + 1} omits ${missing.join(', ')} — ${line.trim().slice(0, 70)}`)
}
continue
}
if (!/_?\btier\b|\b_tier\b/.test(line)) {
out.push(`${label}:${i + 1} ${line.trim().slice(0, 90)}`)
}
}
return out
}
/* .vue AND the build scripts. The fifth occurrence of this bug was in
scripts/refresh-legacy-resource-flags.mjs, which this check never looked at — so it
reported "nothing to change" across 122 curated rows, 8.9 million people, that the index
could already reach. A gate that only watches one half of a bundle certifies the other. */
const SCRIPTS = resolve(DIR, 'scripts')
const surfaces = [
...readdirSync(DIR).filter((f) => f.endsWith('.vue')),
...readdirSync(SCRIPTS).filter((f) => f.endsWith('.mjs')).map((f) => `scripts/${f}`),
]
if (!surfaces.length) {
console.error('SURFACES-READ-THE-TIER: no .vue surfaces found — the check would pass vacuously.')
process.exit(1)
}
if (POISON) {
/* Both real shapes, so the self-test proves the check against the two defects it exists
for rather than one — the second is the line that actually got through. */
const INJECT =
' none: !c.s && !c.f && !c.au && !c.bv && !c.z && !(c.d && c.d.length),\n' +
' const hasAny = (r) => !!(r.scripture_url || r.film_url || r.audio || r.bible_video)\n' +
' const hasAny = (c) => !!(c.s || c.f || c.au || c.bv)'
let caught = 0
for (const f of surfaces) {
const poisoned = `${readFileSync(resolve(DIR, f), 'utf8')}\n${INJECT}\n`
if (offendersIn(poisoned, f).length) caught++
else console.error(` NOT CAUGHT in ${f}`)
}
if (caught === surfaces.length) {
console.log(
`SURFACES-READ-THE-TIER --poison: ${caught}/${surfaces.length} surface(s) caught the injected formula.`,
)
console.log('SURFACES-READ-THE-TIER: SELF-TEST PASS — the check fails as designed.')
process.exit(0)
}
console.error('SURFACES-READ-THE-TIER: SELF-TEST FAIL — the check cannot detect the bug it exists for.')
process.exit(1)
}
const offenders = surfaces.flatMap((f) => offendersIn(readFileSync(resolve(DIR, f), 'utf8'), f))
if (offenders.length) {
console.error(`SURFACES-READ-THE-TIER: FAIL — ${offenders.length} surface line(s) re-derive "has nothing":`)
for (const o of offenders) console.error(` ${o}`)
console.error(' Read the tier the pin index computed (pins[i][6] === 0) instead of listing')
console.error(' the resource fields again — that list goes stale every time a source is added.')
process.exit(1)
}
console.log(
`SURFACES-READ-THE-TIER: PASS — ${surfaces.length} file(s) checked, none re-derives the tier.`,
)