140 lines
6.9 KiB
JavaScript
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.`,
|
|
)
|