internal/ keeps the reason and drops the speaker: no gate quotes or cites the Driver

Every reference to the Driver in internal/ is reworded the same way as in
library/: the reason stays, the speaker and the quote go, an open decision
reads "provisional, pending a product decision", and agent names and decision-
sheet rows are dropped. Where a gate printed the word, its message now names
the reviewer or the ruling instead.

Three spots were code, not comments, and changed with their callers:
- the element census's allowlist field `driver` is now `ruling`, in the gate
  and both presets (same validation: match, ruling, date and card required);
- check:i18n-never-translated's DRIVER-GATED marker is DECISION-PENDING, in
  the entries and in the regex that counts them (its output is otherwise
  byte-identical);
- the package.json description of the census says "ruling".

public-hygiene-check keeps its two patterns for the word: they are what keep
it out of the served folder.
This commit is contained in:
Doxa Maps 2026-09-29 04:28:58 +00:00
parent bfc99ff827
commit d314c44043
77 changed files with 254 additions and 261 deletions

View file

@ -17,7 +17,7 @@
/**
* Derive the composition tier from the `composes` edges, and refuse to disagree with an author.
*
* ★ WHY DERIVE AT ALL (Driver, 2026-09-27). Tiers 3-5 were AUTHORED ONLY — discover.mjs infers just
* ★ WHY DERIVE AT ALL (2026-09-27). Tiers 3-5 were AUTHORED ONLY — discover.mjs infers just
* `single` and `parameterized`, so a composite with no meta.json shipped as capability 1. That is not
* hypothetical: research-dashboard did exactly that while nesting ten views, and the warning in
* detectProfileType exists because of it. A missing file is indistinguishable from a deliberate choice.

View file

@ -19,7 +19,7 @@ const API = 'https://pray.doxa.life/api/people-groups/list?fields=name,slug,wagf
// ── mirror of people-groups-grid.vue applyDropdownFilters + the Fuse-ish search keys ──────
// Dropdown axes match by VALUE (u[field].value === selection) — the exact contract the header
// dropdown -> grid prop coupling relies on. Country is excluded per Driver.
// dropdown -> grid prop coupling relies on. Country is excluded by decision.
function dropdownFilter(recs, { wagfRegion = '', wagfBlock = '', rop1 = '', religion = '' } = {}) {
let out = recs
for (const [f, sel] of Object.entries({ wagf_region: wagfRegion, wagf_block: wagfBlock, rop1, religion })) {

View file

@ -18,8 +18,8 @@
* API. Camera motion in particular (fitBounds/flyTo) has NO API equivalent to mirror; the only
* way to prove "select rows, the map flies to the selection" is to select rows and read the map.
*
* ── THE COMBINATION MATRIX (Driver: "EVERY SINGLE COMBINATION ... log whether it DISPLAYED,
* pass/fail only") ─────────────────────────────────────────────────────────────────────────
* ── THE COMBINATION MATRIX (EVERY SINGLE COMBINATION, logging only whether it DISPLAYED,
* pass/fail) ─────────────────────────────────────────────────────────────────────────
* One line per combination, BOTH halves recorded, pass/fail only. "Displayed" is never inferred
* from a click firing or an element existing — see WHAT COUNTS AS DISPLAYED per pair below.
*

View file

@ -52,7 +52,7 @@ export function devBackends(env = process.env) {
// This server is SUPPOSED to run the engine (a server deployment with the engine switched on): while it
// is down, the plugin says "not running yet" instead of "runs on your own computer".
expected: () => process.env.RUDDER_ENGINE === '1' || process.env.RUDDER_AUDIO === 'browser',
// RUDDER_ENGINE=0 (a server where Rudder is laptop-only, the Driver 2026-09-27): never start it, even if a
// RUDDER_ENGINE=0 (a server where Rudder is laptop-only, 2026-09-27): never start it, even if a
// binary built by an earlier engine-on deploy is still there — it would offer to record a server's mic.
disabled: () => process.env.RUDDER_ENGINE === '0',
},

View file

@ -54,7 +54,7 @@ const MANIFEST = join(ROOT, 'public', TREE_NAME, 'manifest.json')
// defect recur unnoticed in a second place.
//
// NOT COVERED, and it cannot be: the /plugin-designer SKILL carries the same stale citation and
// lives outside this repo, so no gate here can reach it. That is why the fix is a Driver edit; the
// lives outside this repo, so no gate here can reach it. That is why the fix is an edit outside this repo; the
// repo's job is to hold the checkable half (see BUILD-PLUGIN-DESIGNER-SKILL-MEANS-REAL-WORK).
const DOC_DIR = join(ROOT, 'documentation')
const DOC_FILES = existsSync(DOC_DIR)

View file

@ -10,8 +10,8 @@
*
* WHAT THIS ASSERTS, and why each half is here:
* 1. Every profile resolves to a capability 1-5. A 0 or a null means some surface will render a
* tier that does not exist; the Driver saw exactly that as "the Research Gallery shows as
* capability 0".
* tier that does not exist; that is exactly how the Research Gallery came to show as
* capability 0.
* 2. A profile that composes anything derives cap-4, or cap-5 if it composes a cap-4 — and an
* EXPLICIT type must AGREE with that. The declaration stays authoritative; it is not
* overridden, it is checked. A meta that contradicts the structure means the meta or the

View file

@ -2,8 +2,8 @@
/**
* check-csv-export-shape.mjs — the exported CSV must OPEN correctly in a spreadsheet.
*
* THE DEFECT THIS CATCHES (Driver-reported 2026-09-22: "this is me exporting the CSV directly without
* making any edits... it does look a little bit glitched out"). The generator emitted a one-column
* THE DEFECT THIS CATCHES (reported 2026-09-22: a CSV exported directly, with no edits, looked a
* little glitched out). The generator emitted a one-column
* provenance comment ABOVE the header:
* line 0 ... 1 column "# Research Dashboard export — scope: ..."
* line 1 ... N columns slug,name,religion,...
@ -13,8 +13,8 @@
*
* ★ WHAT WAS *NOT* WRONG, AND MUST NOT BE "FIXED" ON SUSPICION — three plausible theories, all
* refuted by measurement before a line was changed:
* - "it is tab-separated" — FALSE. The generator joins with commas; the tabs in the Driver's paste
* were his terminal rendering, not the bytes.
* - "it is tab-separated" — FALSE. The generator joins with commas; the tabs in the pasted report
* were a terminal's rendering, not the bytes.
* - "quoting is broken" — FALSE. esc() is genuinely RFC-4180: it quotes when , " CR or LF appear and
* doubles internal quotes. Embedded commas, quotes AND newlines survive a strict parser round trip.
* - "CSV injection" — already handled; a leading = + - @ is prefixed with a single quote.
@ -128,7 +128,7 @@ const first = parsed[0] || []
if (first.length === 1 && String(first[0]).startsWith('#')) {
fail('line 0 is a one-column "#" comment, so a spreadsheet will use it AS THE HEADER.',
' The scope sentence becomes the column names and the real header becomes data row 1 — which is\n' +
' what the Driver saw as a "glitched" export. Put the provenance line LAST; a trailing one-column\n' +
' what read as a "glitched" export. Put the provenance line LAST; a trailing one-column\n' +
' row is harmless, a leading one destroys the column mapping for the whole sheet.')
}
if (first[0] !== cols[0]) {

View file

@ -5,7 +5,7 @@
* ★ WHY THIS IS NOT `git status` (a real false-pass, 2026-09-20)
* I verified the invariant for several ticks with:
* cd root-dx-life && git status --porcelain | wc -l # -> 0
* and reported that 0 to the Driver as proof. root-dx-life IS NOT A GIT REPOSITORY — it is a
* and reported that 0 as proof. root-dx-life IS NOT A GIT REPOSITORY — it is a
* plain directory clone. `git status` prints "fatal: not a git repository" to STDERR and nothing
* to stdout, so `wc -l` counts 0. THE CHECK RETURNED THE PASS VALUE *BECAUSE IT FAILED*.
* Identical in shape to the settled-tree churn bug (bfs rejected the argument, stderr discarded,

View file

@ -5,15 +5,15 @@
* ★ WHY. Every clone-fidelity gate here compares PROPERTIES of NAMED selectors ("the region card
* matches on 37 of 37 properties"). All of them are blind to an element that exists on only one side,
* because an element nobody named is an element nobody measured. Regions found an extra control and
* an invented bar chart by eye, past eight green gates. The Driver's scope ruling is binary — "it
* exists on their page and must MATCH, or it is a sanctioned addition" — and this executes it.
* an invented bar chart by eye, past eight green gates. The scope ruling is binary: it exists on
* their page and must MATCH, or it is a sanctioned addition. This executes it.
* (Promoted from regions' _probe-element-census-vs-live.mjs, 2026-09-25.)
*
* HOW, per page pair (our leaf vs the live URL): harvest every VISIBLE TEXT NODE through every shadow
* root on both sides (text nodes, not leaf elements — a leaf-element walk lost every country name that
* shares its anchor with a count span and passed vacuously), then diff the two SETS:
* OURS-ONLY, unaccounted an invention — a scope violation → FAIL
* OURS-ONLY, sanctioned on the preset's allowlist, which carries the Driver's words + date + card
* OURS-ONLY, sanctioned on the preset's allowlist, which carries the ruling + date + card
* THEIRS-ONLY a clone omission — something of theirs never ported → FAIL
* DATA DRIFT the same label with different digits (their pages are SSR snapshots, ours
* is live) — reported, not failed
@ -25,9 +25,9 @@
* CENSUS IS BLIND and exits 75. No preset or ad-hoc run is accepted without one.
*
* PRESETS: internal/element-census/<name>.json — { owner, pages:[{name, leaf, live}], neg:{leaf, text},
* sanctioned:[{match, flags, what, driver, date, card, theirs?, theirsFlags?}] }. `theirs` makes the entry a
* sanctioned:[{match, flags, what, ruling, date, card, theirs?, theirsFlags?}] }. `theirs` makes the entry a
* SANCTIONED PAIR: their string for the same control (e.g. our "Hide Filters" / their "Show Filters"),
* excused as an omission only while our half is on the page. An allowlist entry without the Driver's words,
* excused as an omission only while our half is on the page. An allowlist entry without the ruling,
* a date and a card is REFUSED (exit 2): it is evidence, not an ignore list.
*
* USAGE node internal/check-element-census-vs-live.mjs every preset
@ -63,7 +63,7 @@ function validate(p, from) {
p.neg.leaf = p.neg.leaf || p.pages[0].leaf
if (!p.pages.some((pg) => pg.leaf === p.neg.leaf)) p.pages.push({ name: 'neg control page', leaf: p.neg.leaf, live: p.pages[0].live, negOnly: true })
p.sanctioned = (p.sanctioned || []).map((s, i) => {
for (const k of ['match', 'driver', 'date', 'card']) if (!s[k]) bad(`${from}: sanctioned[${i}] has no "${k}" — every allowance carries the Driver's words, the date and the card`)
for (const k of ['match', 'ruling', 'date', 'card']) if (!s[k]) bad(`${from}: sanctioned[${i}] has no "${k}" — every allowance carries the ruling, the date and the card`)
if (!/^\d{4}-\d{2}-\d{2}$/.test(s.date)) bad(`${from}: sanctioned[${i}].date must be YYYY-MM-DD`)
let re; try { re = new RegExp(s.match, s.flags || '') } catch (e) { bad(`${from}: sanctioned[${i}].match is not a regex: ${e.message}`) }
// A SANCTIONED PAIR: `theirs` names THEIR string for the same control in its other state (their
@ -145,7 +145,7 @@ const read = async (url) => {
let violations = 0, omissions = 0, unmeasured = 0, blind = 0
for (const [name, p] of presets) {
console.log(`\n══ ${name} (owner: ${p.owner || '?'}) ══`)
const sanctionedBy = (s) => { const hit = p.sanctioned.find((x) => x.re.test(s)); return hit ? `${hit.what || hit.card} — Driver ${hit.date}: "${hit.driver}"` : null }
const sanctionedBy = (s) => { const hit = p.sanctioned.find((x) => x.re.test(s)); return hit ? `${hit.what || hit.card} — ruled ${hit.date}: ${hit.ruling}` : null }
const oursByLeaf = new Map()
let negSeenInOursOnly = null
for (const pg of p.pages) {
@ -193,7 +193,7 @@ for (const [name, p] of presets) {
console.log(` DATA DRIFT (same label, different figure — their page is an SSR snapshot, ours is live): ${drift.length}`)
for (const [o, t] of drift.slice(0, 10)) console.log(` ~ ours ${JSON.stringify(o.slice(0, 44))} vs theirs ${JSON.stringify(t.slice(0, 44))}`)
}
for (const [t, x] of pairedTheirs) console.log(` ok their "${t.slice(0, 50)}" — the other half of a sanctioned pair (${x.what || x.card}, Driver ${x.date})`)
for (const [t, x] of pairedTheirs) console.log(` ok their "${t.slice(0, 50)}" — the other half of a sanctioned pair (${x.what || x.card}, ruled ${x.date})`)
console.log(` THEIRS-ONLY (ported nowhere on our side): ${theirsOnly.length}`)
for (const s of theirsOnly.slice(0, 25)) console.log(` !! "${s.slice(0, 80)}"`)
if (theirsOnly.length > 25) console.log(` … and ${theirsOnly.length - 25} more`)
@ -207,7 +207,7 @@ await browser.close(); stopSrv()
console.log(`\n── ${violations} unaccounted addition(s) · ${omissions} omission(s) · ${unmeasured} page(s) not measured · ${blind} blind preset(s) ──`)
if (violations || omissions) {
console.log('ELEMENT-CENSUS: FAIL — an unaccounted addition is a scope violation under the Driver\'s binary test; an omission is a clone gap. Read the lists: this reports, the owner adjudicates.')
console.log('ELEMENT-CENSUS: FAIL — an unaccounted addition is a scope violation under the binary scope test; an omission is a clone gap. Read the lists: this reports, the owner adjudicates.')
process.exit(1)
}
if (blind || unmeasured) { console.log('ELEMENT-CENSUS: NOT MEASURED — a page did not render or the census could not see its own negative control.'); process.exit(NOT_MEASURED) }

View file

@ -2,7 +2,7 @@
/**
* check-embed-url-absolute — a share/embed url must never be relative.
*
* THE DEFECT (Driver, 2026-09-28, testing live): the link copied from Simple Research was
* THE DEFECT (2026-09-28, found testing live): the link copied from Simple Research was
* `research-map-plus-grid/research-map-plus-grid/index.html` while the page was
* `http://127.0.0.1:5192/#f=…` — "the perfect example of how embeddability is not working".
*
@ -42,7 +42,7 @@ const LEAF = 'research-map-plus-grid/research-map-plus-grid/index.html'
const got = absoluteEmbedUrl(LEAF)
if (!/^https?:\/\//i.test(got)) {
fails.push(`absoluteEmbedUrl returned a RELATIVE url with no tree root: "${got}".\n`
+ ' That is the exact value the Driver copied. It must resolve against the runtime origin.')
+ ' That is the exact value that was copied. It must resolve against the runtime origin.')
}
if (got !== 'http://127.0.0.1:5192/' + LEAF) {
fails.push(`absoluteEmbedUrl did not resolve against the page origin: "${got}"`)

View file

@ -59,7 +59,7 @@ const BASE = process.argv.slice(2).find((a) => !a.startsWith('--'))
|| process.env.BASE_URL || 'http://127.0.0.1:4173'
const HOST_PORT = Number(process.env.RTL_HOST_PORT || 47393)
// ★ --emit WRITES THE PARTNER PAGE TO DISK INSTEAD OF MEASURING IT.
// Under the Playwright policy (Driver, 2026-09-25) only rega runs a browser, so this check's
// Under the Playwright policy (2026-09-25) only the browser seat runs a browser, so this check's
// result now has to be produced by someone else's eyes. But the partner page only ever existed
// INSIDE this process — two createServer calls on ephemeral ports — so "open a partner page at a
// foreign origin" was not a thing anyone could actually do. NEEDS-EYES rows 3, 4 and 6 name a

View file

@ -2,10 +2,9 @@
/**
* check-facet-cascade.mjs — A CHILD FACET SELECTION MUST NOT OUTLIVE ITS PARENT.
*
* THE DEFECT THIS CATCHES (Driver-reported 2026-09-22): "if you have a language selected and then
* you deselect the language family, it does not deselect the language of that language family that
* was selected... because of that I tried to go and select a new language family and it does not
* show up on the map. And I cannot see the reason why unless I look at the selected section header."
* THE DEFECT THIS CATCHES (reported 2026-09-22): with a language selected, deselecting its language
* family did not deselect the language. Selecting a new language family then showed nothing on the
* map, and the only visible reason was in the selected section header.
*
* ★ WHY THIS IS A DATA-TRUST BUG, NOT A NIT. filterSet is AND-stacked — filteredRows keeps a row
* only when EVERY active key matches. An orphaned child keeps filtering, so after the parent is
@ -25,7 +24,7 @@
* leaving the identical orphan one rung down, so depth is asserted explicitly.
* B. NO ORPHANS — for each hierarchy, set every tier, clear the TOP, and assert no descendant
* key survives in filterSet.
* C. THE DRIVER'S ACTUAL SEQUENCE, asserted on counts (the step that reproduces what he saw):
* C. THE REPORTED SEQUENCE, asserted on counts (the step that reproduces what was seen):
* 1. select parent P, then child C within P -> record N1 (must be > 0)
* 2. clear P -> assert C is gone from filterSet
* 3. select a DIFFERENT parent Q -> assert count > 0 AND == Q-alone count
@ -42,7 +41,7 @@
* NEGATIVE-TESTED, and the negative test is part of this file: `--self-test` copies the bus, strips
* the cascade line back out, re-runs every assertion against the reverted copy and requires the
* gate to FAIL. Measured: 13 assertions fail, including "after switching to Q=… the map is EMPTY
* (Showing 0)" for all three hierarchies — i.e. it reproduces the Driver's report exactly. A gate
* (Showing 0)" for all three hierarchies — i.e. it reproduces the report exactly. A gate
* nobody has broken is a gate nobody knows works.
*
* ── HERMETIC BY CONSTRUCTION (it runs inside build:unlocked) ──────────────────────────────────
@ -55,7 +54,7 @@
*
* A MISSING FIXTURE IS A FAILURE, NOT A SKIP. An earlier draft fetched live rows and skipped the
* count assertions when offline — so on a fresh clone with no network, C3 (the assertion that
* reproduces the Driver's empty map) silently did not run and the build still went green. That is
* reproduces the reported empty map) silently did not run and the build still went green. That is
* a gate that cannot fail, which is worse than no gate. Now every assertion runs everywhere.
*/
import { readFileSync, writeFileSync, existsSync, mkdirSync, rmSync, cpSync } from 'node:fs'
@ -166,7 +165,7 @@ const EXPECT = {
//
// WHY A FIXTURE AND NOT "fetch, else skip". The earlier version fetched live rows and SKIPPED the
// count assertions when offline — which meant that on a fresh clone with no network, C3 (the
// assertion that actually reproduces the Driver's empty map) silently did not run and the build
// assertion that actually reproduces the reported empty map) silently did not run and the build
// still went green. That is the "a gate that cannot fail is worse than no gate" shape: decorative
// in exactly the situation where it matters. With the fixture committed, every assertion runs
// everywhere, always, in ~1s.
@ -328,11 +327,11 @@ async function run(busPath) {
}
}
// ── C. the Driver's sequence, asserted on RESULT COUNTS ─────────────────────────────────────
// ── C. the reported sequence, asserted on RESULT COUNTS ───────────────────────────────────
if (!rows) {
// NOT a skip. The committed fixture is what makes C/D run everywhere, so its absence is a
// BROKEN GATE, not an excusable environment gap — silently skipping the assertions that
// reproduce the Driver's bug is how a gate becomes decorative. Only --offline with LIVE=1
// reproduce the reported bug is how a gate becomes decorative. Only --offline with LIVE=1
// (an explicit opt-in to the live dataset with no cache) is allowed to degrade.
if (process.env.LIVE && OFFLINE) notes.push(' SKIP C/D — LIVE=1 --offline with no cache (explicitly opted out of the fixture)')
else bad('C/D count assertions could not run', `missing fixture ${FIXTURE} — regenerate with --refresh-fixture`)

View file

@ -5,14 +5,14 @@
* ★ WHAT IT ASSERTS, and why each assertion exists rather than the obvious one.
*
* THE OBVIOUS ASSERTION — "selecting a facet changes the map's feature COUNT" — PASSES TODAY AND
* PASSED WHILE THE DRIVER COULD SEE THE BUG. That is measured, not hypothetical: selecting
* PASSED WHILE THE BUG WAS PLAINLY VISIBLE. That is measured, not hypothetical: selecting
* "Has Prayer" already narrowed rd-bubbles 2107 -> 689 features, disjoint from "Needs Prayer"
* (1418), summing exactly to 2107 and round-tripping on clear. The filter was never broken.
* What was broken is that every dot was painted the SAME flat orange ('circle-color' read back
* off the live layer as the bare string '#ef6c00', not an expression), so a prayer question got
* an answer with no prayer information in it. The Driver's words are a COLOUR request:
* "when I sort by has prayer it needs to SHOW ME WHICH ONES HAVE PRAYER AND WHICH ONE…
* The dots are not changing based on that filter."
* an answer with no prayer information in it. The report was a COLOUR request:
* sorting by Has Prayer must SHOW WHICH ONES HAVE PRAYER AND WHICH DO NOT, and the dots
* were not changing with that filter.
* So this gate asserts BOTH halves, because either one alone is a false pass:
* 1. SET — the selection changes WHICH dots are on the map (count + identity).
* 2. MEANING — the surviving dots all genuinely satisfy the selected level.
@ -20,14 +20,14 @@
* not a flat literal, and it reads the property the features actually carry.
*
* ★ ASSERTION 3 IS THE ONE THAT WOULD HAVE CAUGHT THE REPORTED BUG. Assertions 1 and 2 were
* already green when the Driver reported it.
* already green when it was reported.
*
* ★ IDENTITY, NOT JUST COUNT. The first five slugs are BYTE-IDENTICAL before and after, because
* the filter removes from the tail of an alphabetical list. A gate that eyeballed the head of the
* list, or that compared only counts, would report "looks the same" / "looks fine" for the wrong
* reason. We compare SETS of slugs and require a strict, non-empty subset relation.
*
* ★ WHY THE COLOUR CHECK IS CONDITIONAL. Orange (#ef6c00) is a Driver decision with a contrast
* ★ WHY THE COLOUR CHECK IS CONDITIONAL. Orange (#ef6c00) is a product decision with a contrast
* analysis behind it and is correct with NO prayer filter active; the 3-tier prayer ramp is
* correct WITH one. So the gate requires a literal before and an expression after — asserting the
* SWITCH, which is the actual contract, rather than hardcoding either colour as always-right.
@ -96,7 +96,7 @@ const PATH_ = process.env.PROBE_PATH || '/'
const TAB = process.env.PROBE_TAB || 'research-dashboard'
const SELF_TEST = process.argv.includes('--self-test')
// WHICH HALF OF THE FIX TO REVERT in --self-test. Default 'colour' reverts ONLY the paint, leaving
// the feature properties intact — that is the EXACT bug the Driver reported (a correct filter whose
// the feature properties intact — that is the EXACT bug reported (a correct filter whose
// dots all look the same), and it isolates assertion 3, the one assertions 1-2 cannot catch.
// 'props' strips peoplePraying instead; 'both' does the lot; 'frozen' blocks the source update
// entirely, simulating "the filter never reaches the map" and negative-testing assertion 1.

View file

@ -8,8 +8,8 @@
* controls for one axis — a "People" dropdown beside an "Affinity Bloc" tree tier — which could
* never recognise each other, because filteredRows byte-matches each key against its OWN accessor.
* The tree stored affinity_bloc='A012' while the dropdown stored rop1='South Asian Peoples', and
* neither ever saw the other's selection. Driver, from behaviour alone: "there are duplicate
* affinity blocks, one called People, one called Affinity Bloc... they are the same thing."
* neither ever saw the other's selection. Seen from behaviour alone: there were duplicate
* affinity blocks, one called People and one called Affinity Bloc, and they are the same thing.
*
* ★ WHY THIS GATE ASSERTS ON OUTPUT VALUES AND NOT ON FIELD READS — THE MEASUREMENT THAT DECIDED IT.
* The obvious gate is "no two facets may read the same r.<field>". THAT GATE WOULD FAIL CORRECT

View file

@ -7,7 +7,7 @@
* `<strong>{{ selectedOption.label }}</strong>` appeared in the label. Measured before the fix: the
* row grew 960 → 1294px and individual triggers reached 240px.
*
* ★ WHY IT IS NOT COSMETIC, in the Driver's own reasoning: the selections row with Clear All sits
* ★ WHY IT IS NOT COSMETIC: the selections row with Clear All sits
* UNDERNEATH the filters. If a filter grows on selection, the row below re-flows and the whole group
* stops being the aligned block he asked for. The stable width is what makes the centred stack hold
* still — so this is a layout invariant, not a preference about widths.

View file

@ -4,10 +4,10 @@
*
* ★ WHY THIS EXISTS (LL-the-user-found-three-bugs-my-gates-could-not-see, 2026-09-23). The suite
* was 56/58 green, verify:gate1 13/13, verify:map-paint 11/11, verify:interop 4/4, 363 done cards
* — and the Driver opened the product and found FIVE real defects in one sitting, several caused
* — and opening the product found FIVE real defects in one sitting, several caused
* by a change a gate had certified. NOT ONE was visible to any gate, because every gate up to this
* one asserts STATE (does it mount, does it fetch, do the counts agree, does a canvas paint). The
* Driver assesses FITNESS FOR USE: is it slim, can I see it at once, does the wheel do what I
* product is judged on FITNESS FOR USE: is it slim, can I see it at once, does the wheel do what I
* expect, does it tell me what I'm looking at. A suite of the first kind can be perfectly green
* while the second kind fails completely. This gate encodes the second kind, as five clauses:
*
@ -233,7 +233,7 @@ async function runComposite() {
// ── CLAUSE A — ONE-VIEWPORT RULE ─────────────────────────────────────────────────────────────
// .stl-sec (StackLayout's own section wrapper, library/layouts/StackLayout.vue:62) is the band
// the Driver actually sees stacked above the grid — measuring .rd-shell alone would miss a
// a reader actually sees stacked above the grid — measuring .rd-shell alone would miss a
// regression in the SECTION wrapper around it (exactly what the self-test above pokes).
//
// ★ THE CAUSAL TEST, NOT A STATIC SIZE COMPARISON (rewritten 2026-09-23, BUG-DASHBOARD-BAND-
@ -290,8 +290,8 @@ async function runComposite() {
record('A', TARGET, fitsViewport, detail)
if (!fitsViewport) fail('a', `dashboard band is ${band.bandH}px, taller than the ${VIEWPORT_H}px viewport — it sizes to its own content instead of the screen. ` + detail)
// Driver, verbatim: "the dashboard rendering to the full height of the tree legend" is the
// bug — i.e. the band TRACKING the legend's content, not merely being numerically larger
// THE BUG: the dashboard rendering to the full height of the tree legend, i.e. the band
// TRACKING the legend's content, not merely being numerically larger
// than it. Test that directly: expand every section (worst case, ~2100px of rows) and assert
// the band does not grow past the viewport with it. GROWTH_TOLERANCE_PX is generous enough to
// absorb a scrollbar-width reflow, tight enough that the original 2325px regression (a ~1400px

View file

@ -10,7 +10,7 @@
* component read:
* STANDALONE research-dashboard ....... dLeft=0 dTop=4 (six of six)
* INSIDE plugin-studio dashboard tab .. dLeft=256 dTop=111 (every one)
* The Driver reported the menu appearing "in a random spot further down to the right" for days. It
* The menu was reported for days as appearing in a random spot further down to the right. It
* was never random. 256 is the rail width, 111 the header height.
*
* ★ WHY NO EXISTING GATE SAW IT, AND WHY THAT IS THE INTERESTING PART. A probe DID exist and it DID

View file

@ -499,7 +499,7 @@ function scanBoundExpr(expr, file, attrName, out, line = 0, decls = '') {
}
// ★ dir="ltr" MARKS TEXT THAT IS MEANT TO STAY ENGLISH. gospel's plugins use it as the island
// marker for content the Driver ruled stays English (a BLUF, a source citation), so flagging it as a
// marker for content ruled to stay English (a BLUF, a source citation), so flagging it as a
// leak would ask someone to "fix" a deliberate decision. Such findings are marked deliberate, listed
// SEPARATELY, and excluded from the count a gate would fail on — visible, but not a defect.
// Inherited down the subtree: a dir="ltr" wrapper covers everything inside it.
@ -964,7 +964,7 @@ if (composed.length) {
for (const f of leaks) console.log(` ${f.kind.padEnd(16)} ${f.file.replace(/^.*plugin-designs\//, '')}:${f.line ?? 0} ${JSON.stringify(f.text).slice(0, 90)}`)
if (deliberate.length) {
// Listed so nobody wonders where they went, but NOT counted: dir="ltr" is the plugin's own marker
// for content the Driver ruled stays English. Asking someone to "fix" these would be asking them
// for content ruled to stay English. Asking someone to "fix" these would be asking them
// to undo a decision.
// PER ISLAND, LARGEST FIRST. A big island is not automatically wrong, but it is the shape an
// over-broad one has, and a flat total cannot show it. Size is reported in LINES as well as

View file

@ -3,16 +3,14 @@
* check-heartbeat-threshold.mjs — THE HEARTBEAT THRESHOLD MUST COME FROM CONFIG, NOT A LITERAL.
*
* WHAT A HEARTBEAT ZONE IS. The top bin of the research-dashboard choropleth: the countries
* holding the most people groups. Driver: "the heartbeat zones are the most people groups in
* them... that's going to be 150 plus people groups in those heartbeat zones."
* holding the most people groups: 150 or more in each heartbeat zone.
*
* ★ THE DEFECT THIS CATCHES, AND WHY NOTHING ELSE COULD. The ramp's edges were a 5-stop array
* literal — `upTo: 5, 20, 60, 150, Infinity` — sitting in DashboardMapPanel.vue. Those numbers are
* not a property of choropleths; they are a property of THIS DATASET (~2000 people groups). Point
* the dashboard at a 200-group dataset and every country falls in the bottom bin: the map renders
* uniformly pale and says nothing. Driver: "that filter of 150 ... should be a variable. It
* shouldn't be a static ... I need to be able to reuse this dashboard for other maps, other data
* sets." A dataset assumption wearing the costume of a constant.
* uniformly pale and says nothing. The 150 has to be a variable, not a static, so the dashboard can
* be reused for other maps and other datasets. A dataset assumption wearing the costume of a constant.
*
* That failure is INVISIBLE to every other gate in this repo. The code is correct, it parses, it
* builds, it renders, it throws nothing, and the screenshot looks like a map. Only the MEANING is
@ -71,7 +69,7 @@
* ★ THE FIRST VERSION OF THIS GATE SHIPPED WITH [literal] ONLY, and its header said it had been
* "negative-tested" — true of that case, misleading overall. Testing that THE FEATURE WORKS (a
* configured threshold moves the edge) is NOT testing that THE REGRESSION IS CAUGHT (the default
* silently changing), and only the second protects the Driver. One negative test per regression
* silently changing), and only the second protects the shipped map. One negative test per regression
* claimed. A gate that cannot fail is worse than no gate — this repo has shipped one.
*
* NOT COVERED, deliberately: this gate reasons about BIN ASSIGNMENT and LABELS only. It says
@ -93,8 +91,8 @@ const SELF_TEST = process.argv.includes('--self-test')
//
// ★ THE TOP LABEL IS MATCHED BY PROPERTY, NOT BY EQUALITY — a correction this gate earned on its
// FIRST RUN. It originally demanded the top label equal exactly "150+", and immediately failed
// CORRECT code: the label reads "150+ · heartbeat zone", because naming the band in the Driver's
// own words is a requirement of this work, not a defect. An over-strict gate that fires on correct
// CORRECT code: the label reads "150+ · heartbeat zone", because naming the band in the
// specified words is a requirement of this work, not a defect. An over-strict gate that fires on correct
// code gets switched off, and then the real regression walks through. So the assertion is the
// property that actually matters: the top label must CARRY THE CURRENT THRESHOLD (so a moved edge
// can never leave a stale number on the legend) and must NAME THE ZONE. Any prose around those is
@ -127,7 +125,7 @@ function makeResolver(mapCfg, srcOverride) {
const body = srcOverride !== undefined ? srcOverride : resolverSource()
// The slice calls the panel's `T(key, en)` (DashboardMapPanel.vue: `const T = (key, en) => tStatic(key)
// || en`), which lives above the slice. Hand it the same shape answering in English, so the "names
// the zone" assertion still reads the Driver's words.
// the zone" assertion still reads the specified words.
const fn = new Function('cfg', 'T', body + '\nreturn resolveBins;')
return fn({ map: mapCfg }, (key, en) => en)
}
@ -218,7 +216,7 @@ function run(srcOverride) {
expectHistorical(def, 'A', 'With no config supplied the panel must fall back to EXACTLY the historical ramp.')
// ── A2. THE SHIPPED CONFIGURATION RENDERS THE HISTORICAL RAMP ───────────────────────────────
// ★ THIS IS THE ASSERTION THAT PROTECTS THE DRIVER, AND IT WAS MISSING. A alone proves the
// ★ THIS IS THE ASSERTION THAT PROTECTS THE SHIPPED MAP, AND IT WAS MISSING. A alone proves the
// PANEL'S FALLBACK is intact — a strictly weaker property than "the map still looks the same",
// because the running app resolves with dashboard.config.js's map.* layered ON TOP of that
// fallback. Assertion F only checked that heartbeatThreshold was DECLARED, never what it was
@ -293,7 +291,7 @@ function run(srcOverride) {
// ★ TWO CASES, BECAUSE THERE ARE TWO DISTINCT REGRESSIONS AND THEY ARE NOT THE SAME ASSERTION.
// This gate originally shipped with case 1 only, and its header claimed it was "negative-tested".
// That was true of case 1 and MISLEADING overall: case 2 — the one that actually protects the
// Driver's "today's map must not change" requirement — was unasserted, and a one-line edit to
// "today's map must not change" requirement — was unasserted, and a one-line edit to
// dashboard.config.js changed the shipped ramp while the gate printed PASS. Verified by
// reproducing it, not by reasoning about it.
//

View file

@ -65,7 +65,7 @@ const ALLOWED = new Map([
'file-format name — "JSON" names the format the button downloads; the file extension is the same in every locale'],
['poster.json:dpi',
'unit symbol — "DPI" is how print resolution is labelled in every one of these locales\' print dialogs; kept as-is by project decision (the poster-strings landing)'],
// ── DRIVER-GATED, NOT RESOLVED ──────────────────────────────────────────────
// ── DECISION-PENDING, NOT RESOLVED ───────────────────────────────────────────
// ⚠ THESE ARE ALLOWED SO THE GATE CAN GO GREEN ON THE THINGS IT HAS ACTUALLY SETTLED, NOT
// because the defect is fixed. "UUPGs" really is an English acronym wedged into an otherwise
// localized row, in Arabic and Chinese included, while `population` beside it translates
@ -76,11 +76,11 @@ const ALLOWED = new Map([
// an acronym produced invented initialisms (PNA, UEVG, ННГ, GNA) with no source, and a wrong
// term looks AUTHORITATIVE to a reader who cannot check it.
// See DECIDE-UUPGS-COLUMN-HEADER-IS-ENGLISH-IN-ALL-ELEVEN-LOCALES, which carries the priced
// options. REMOVE THESE TWO LINES when the Driver rules — they are a bookmark, not a verdict.
// options. REMOVE THESE TWO LINES when the decision lands — they are a bookmark, not a verdict.
['legend.json:columns.upgs',
'DRIVER-GATED: needs the field\'s term per locale, not an invented acronym — see DECIDE-UUPGS-COLUMN-HEADER'],
'DECISION-PENDING: needs the field\'s term per locale, not an invented acronym — see DECIDE-UUPGS-COLUMN-HEADER'],
['prayerLaps.json:upgs',
'DRIVER-GATED: same term as legend.json:columns.upgs — see DECIDE-UUPGS-COLUMN-HEADER'],
'DECISION-PENDING: same term as legend.json:columns.upgs — see DECIDE-UUPGS-COLUMN-HEADER'],
// ⚠ SETTLED, NOT A BOOKMARK — unlike the two above, this one is not waiting on a ruling; it IS
// the ruling. "UPG 100" is the product's chosen NAME for the prayer list, so it is a brand
// designation and correctly identical in every locale, exactly like a version number. The
@ -171,10 +171,10 @@ if (findings.length) {
process.exit(1)
}
// ★ "ALLOWED" IS NOT ONE THING, and collapsing the two would make this gate lie. A proper noun
// is SETTLED — it is correct English forever. A DRIVER-GATED key is an OPEN DEFECT parked so the
// is SETTLED — it is correct English forever. A DECISION-PENDING key is an OPEN DEFECT parked so the
// gate can still catch the next regression; calling it "identical on purpose" would turn a
// deferral into a verdict, which is exactly how an open item stops being visible.
const deferred = allowedHits.filter((a) => /DRIVER-GATED/.test(a.why))
const deferred = allowedHits.filter((a) => /DECISION-PENDING/.test(a.why))
if (deferred.length) {
console.log(`\n ⚠ ${deferred.length} DEFERRED, NOT SETTLED — these ARE untranslated and are waiting on a ruling:`)
for (const a of deferred) console.log(` ${a.id} = "${a.value.slice(0, 40)}"`)

View file

@ -2,7 +2,7 @@
/**
* check-i18n-stopped-scope.mjs — createAppI18n() must succeed while a STOPPED effect scope is current.
*
* WHY (P0, the Driver's console, 2026-09-27): in Plugin Studio's gallery every tab after the first threw
* WHY (P0, seen in a live console, 2026-09-27): in Plugin Studio's gallery every tab after the first threw
* "SyntaxError: Unexpected error" from createI18n inside createAppI18n. vue-i18n's createGlobal() runs in
* scope.run() and throws UNEXPECTED_ERROR when that returns undefined; Vue 3.5 creates a new EffectScope
* INACTIVE when the currently active one is stopped; and a custom element's connectedCallback ->

View file

@ -7,8 +7,8 @@
// browse the UPG-100 lists and cannot export a packet or a poster. No error, nothing to click; the
// failure is invisible rather than broken, which is why it survived.
//
// The packet IS the product for this profile. The Driver: "print options at the top right of the
// legend... that's the only way that map would be usable."
// The packet IS the product for this profile, so the print options sit at the top right of the
// legend: without them that map is not usable.
//
// ⚠ THE CONTROL IS GATED ON A SELECTION. `v-if="SHOW_EXPORT_BUTTON && exportEnabled && selectedNode"`
// — so a probe that merely loads the page finds nothing at EITHER width and concludes "no export

View file

@ -14,9 +14,8 @@
// LABEL beside it went through the catalog. Measured 2026-09-24 on the served leaf:
// ?lang=es changed 2 of 76 rendered strings ("POPULATION" -> "POBLACIÓN") and left all
// three tabs AND all three info popovers in English. The mechanism worked; the strings
// were never catalogued. The Driver named this affordance specifically — "the tab itself
// gives you a little information button that will explain what a UPG-100 list is" —
// which is exactly the sentence a reader who does not read English cannot use.
// were never catalogued. This affordance was named specifically — the tab carries a small
// information button that explains what a UPG-100 list is — which is exactly the sentence a reader who does not read English cannot use.
//
// ⚠ IT RENDERS THE REAL PAGE. A catalog-only check cannot tell a shipped translation
// from a dead one, and a reimplemented lookup cannot tell you the component actually

View file

@ -87,8 +87,8 @@ const EXPECT = {
// and 5 have FOUR each. The "or Resources" clause is not literally true in the data, so that phrase
// must never be repeated to a mission team as fact.
//
// NOT asserted as a gate condition here, deliberately: which figure is OFFICIAL is a Driver decision
// (DRIVER-DECISIONS-gospel-resources.md, Decision 1), and a gate must not quietly canonise one answer
// NOT asserted as a gate condition here, deliberately: which figure is OFFICIAL is provisional,
// pending a product decision, and a gate must not quietly canonise one answer
// to a question that is still open. Recorded so the next reader inherits the measurement, not the
// task of re-deriving it.

View file

@ -2,12 +2,12 @@
/**
* check-legend-top-offset — the legend only reserves room for a toolbar that is actually drawn.
*
* THE DEFECT (Driver, 2026-09-28): on Simple Research's Regions and Languages tabs under MapLibre, the
* THE DEFECT (2026-09-28): on Simple Research's Regions and Languages tabs under MapLibre, the
* search bar is gone but the legend does not rise into the vacated space. The other tabs are fine.
*
* ★ THE TWO-TAB SPLIT WAS A PREDICATE, NOT A COINCIDENCE. With no geocoder the legend rises to 14px
* unless showsFlyTools() is true, and that returns true for exactly 'doxa-regions', 'language-family'
* and 'language-families' — the Driver's two tabs. It stops at 56px to avoid a MEASURED collision on
* and 'language-families' — the two reported tabs. It stops at 56px to avoid a MEASURED collision on
* country-posters, where LegendTools renders at top:14px, the legend's own slot.
*
* ★ BUT showsFlyTools() DESCRIBES THE LEGEND TYPE, NOT WHETHER THE TOOLBAR EXISTS. Fly + Clusters are
@ -17,7 +17,7 @@
* of empty map on both tabs, no toolbar anywhere in the strip.
*
* So the offset must be keyed on the toolbar's PRESENCE. Keying it on the tab name would have fixed the
* Driver's two tabs and reintroduced the country-posters collision the moment the tools were shown.
* two reported tabs and reintroduced the country-posters collision the moment the tools were shown.
*
* Exit 0 pass · 1 fail.
*/
@ -42,7 +42,7 @@ if (!m) {
}
if (!/showLegendTools/.test(body)) {
fails.push('legendTopVar does not consult showLegendTools, so it reserves space for a toolbar that\n'
+ ' may never be drawn. That is the Driver-reported dead band on the Regions and Languages\n'
+ ' may never be drawn. That is the reported dead band on the Regions and Languages\n'
+ ' tabs: the composite passes legendTools:false and the 56px held a slot for nothing.\n'
+ ' Key the offset on the toolbar\'s PRESENCE, not on the legend type alone.')
}

View file

@ -22,8 +22,8 @@ import { leavesOf, zoneIdsIn, withUnassigned, selectedId, UNASSIGNED_ID } from '
// groupings.json now live in data/tables/ — the shared asset pool the tree copies into
// public/dx-maps/assets/tables/ — because gospel's legend renders IN THE BROWSER and needs them at
// runtime; a build-time import path would not reach the bundle. admin-to-list.json stays in the
// plugin until the Driver rules row 87 (the "zones fold" regeneration), so it is read from the old
// root. Collapsing these to one constant silently breaks whichever file is not where you pointed.
// plugin, provisional, pending a product decision on the "zones fold" regeneration, so it is read
// from the old root. Collapsing these to one constant silently breaks whichever file is not where you pointed.
const REPO = join(dirname(fileURLToPath(import.meta.url)), '..')
const TABLES = join(REPO, 'data', 'tables')
const PLUGIN_DATA = join(REPO, 'plugin-designs', 'my-upg-100-list', 'data')
@ -122,9 +122,9 @@ ok(withUnassigned(EMPTYZONE, ['z9']).length === EMPTYZONE.length,
// evidence lives — after catching the identical shape in their own work.
//
// This does NOT resolve the three disagreeing lists (33 in zone-regions, 12 reachable via
// admin-to-list, 14 in groupings, disagreeing in BOTH directions). That is a data-ownership ruling
// for upg100 and the Driver, and picking a winner here would be exactly the silent join we have both
// refused. It only compares the region tree's own source against the joinable set and RECORDS the
// admin-to-list, 14 in groupings, disagreeing in BOTH directions). That is a data-ownership question,
// provisional, pending a product decision, and picking a winner here would be exactly the silent join
// this repo refuses. It only compares the region tree's own source against the joinable set and RECORDS the
// difference, so the disagreement cannot drift without someone being told.
const regionOf = readJson('zone-regions.json') // the region tree's source: zone id -> region
const adminToList = readJson('admin-to-list.json') // admin name -> zone id; its VALUES are joinable

View file

@ -8,8 +8,8 @@
* discovered but MISSING .... 1 key-cities
* in manifest, not real ..... 1 template-bundle
* `key-cities` had its compiled key-cities.js on disk but ZERO profile leaf directories, so its four
* profiles vanished from manifest.json and from the studio's tab list. The Driver was reviewing a tree
* that silently omitted a whole plugin.
* profiles vanished from manifest.json and from the studio's tab list. A tree under review
* silently omitted a whole plugin.
*
* ★ WHY EVERY EXISTING GATE PASSED — this is the interesting part, and the reason for a new file:
* - the settled-tree LEAF FLOOR counts index.html recursively (44, floor 40). A bundle that loses

View file

@ -3,7 +3,7 @@
* check-map-menu-anchor — a map control menu must always have an anchor, so it can never fall to its
* static position off the edge of the page.
*
* THE DEFECT (Driver, 2026-09-28, mobile): the share/embed menu opens OFF-SCREEN to the right on Cities
* THE DEFECT (2026-09-28, mobile): the share/embed menu opens OFF-SCREEN to the right on Cities
* with Internet and Gospel Resources, while Simple Research and Prayer Progress are correct.
*
* ★ THE SPLIT WAS AN ALLOWLIST, NOT A PLUGIN DIFFERENCE. MapControlMenu resolved its containing area
@ -14,8 +14,8 @@
* that happened to be named in a string literal in another component.
*
* MEASURED: cities-internet, gospel-resources, key-cities and upg-neighbor carry NO map-area class and
* no data-map-area. The Driver reported two; the allowlist made it four. A gate that only checked the
* two he named would have passed the other two.
* no data-map-area. Two were reported; the allowlist made it four. A gate that only checked the
* two reported would have passed the other two.
*
* SO THIS ASSERTS THE PROPERTY, NOT THE LIST: whatever a plugin calls its map area, the menu resolves
* SOMETHING to anchor against. The fallback is the toolbar's own offsetParent — the containing block the
@ -45,7 +45,7 @@ if (!m) {
+ ` ${expr.trim()}\n`
+ ' A plugin whose map area is not named in that list gets a null area, place() returns an\n'
+ ' EMPTY style, and the menu renders at its static position — off the page. That is the\n'
+ ' Driver-reported mobile bug, and it is decided by a string literal rather than by geometry.')
+ ' reported mobile bug, and it is decided by a string literal rather than by geometry.')
}
}

View file

@ -4,7 +4,7 @@
*
* THE BUG THIS EXISTS TO PREVENT (real, found 2026-09-22 by resizing the cap-5 SPA to 414px).
* VerticalTabsLayout renders its drawer toggle inside `<header v-if="header">`. The cap-5 DX-Maps
* SPA runs header-OFF by default (the Driver's no-stacked-headers rule for embedding). At mobile
* SPA runs header-OFF by default (the no-stacked-headers rule for embedding). At mobile
* width the rail becomes an off-canvas drawer at translateX(-100%) with navOpen=false. So: the
* drawer was off screen, the header holding the toggle did not exist, and the only other control
* (.vtl-collapse) lives INSIDE the drawer — measured off screen at x:-244.

View file

@ -3,7 +3,7 @@
* check-no-orphan-gates.mjs — a gate that nothing can run is not a gate.
*
* THE DEFECT THIS CATCHES. `internal/verify-shell-scroll.mjs` was written, negative-tested, and
* proved a real Driver-reported bug with hard numbers (2586px of mounted grid, a real wheel gesture
* proved a real reported bug with hard numbers (2586px of mounted grid, a real wheel gesture
* moving 0px). Then it sat unreachable: no `package.json` entry, no importer. It would never have
* run again. The repo has already carded this once —
* BUILD-BROWSER-GATES-WERE-ALL-ORPHANS-ONE-COMMAND-RUNS-THEM — and it recurred anyway, because

View file

@ -14,10 +14,9 @@
* ★ WHY THIS IS THE INTERESTING CLASS. The standing rule "keep the box closed" lived in prose, in
* instructions to an agent. The actual behaviour lived in a FLAG in package.json. Prose cannot
* constrain a flag. The rule was never violated by a decision — it was violated by a default that
* nobody re-read, and it stayed violated because nothing ever asked the question out loud. The
* Driver stated it plainly: "we don't need any open ports on this box because you push your changes
* to Forgejo and I pull them from my laptop... use Playwright for yourself internally for testing,
* not for public access."
* nobody re-read, and it stayed violated because nothing ever asked the question out loud. It was
* stated plainly: this box needs no open ports, because changes are pushed to Forgejo and pulled
* onto a laptop; Playwright is for internal testing, not for public access.
*
* Loopback is sufficient for the entire test suite: verify-browser-all, check:a11y-button-names,
* check:nav-reachable and smoke:share-all all already default to 127.0.0.1. The wide bind bought

View file

@ -3,7 +3,7 @@
* check-one-scroll-container — a plugin pinned into the gallery's pane must not assert a height FLOOR
* the pane cannot honour.
*
* THE DEFECT THIS EXISTS FOR (Driver, 2026-09-28, "one of my least favorite bugs"): on DX Life's
* THE DEFECT THIS EXISTS FOR (2026-09-28, a much-disliked bug): on DX Life's
* research page a capability-4 plugin with tabs at the top had an EXTRA element-level scrollbar, and
* scrolling it scrolled the TAB HEADER out of view — on top of the plugin's own content scroll.
*

View file

@ -141,8 +141,8 @@ const SITES = [
// wave 3d — all-people-groups. Treated as UNOWNED (studio agreed), so the conservative reading wins:
// its inline EN map stays the fallback rather than switching to the catalogue, because nobody is
// available to decide that this bundle should start honouring the locale on a bare host -- THEN
// dx-shepherd RULED the locale-honouring fallback everywhere (2026-09-27; the Driver's goal (d), a
// bare-host embed with ?lang=ar must render Arabic, now the embed contract in COMMON.md). So it takes
// the locale-honouring fallback was RULED for everywhere (2026-09-27; the goal: a bare-host
// embed with ?lang=ar must render Arabic, now the embed contract in COMMON.md). So it takes
// the catalogue default, its duplicate EN map is deleted, and section 9 keeps the ruling's PRECONDITION
// true rather than measured once.
// First site on the {t, te, locale} form: it needs locale for toLocaleString.

View file

@ -20,7 +20,7 @@
// black with no error of any kind. Asserted as a WHOLE-MAP property, because the first
// attempt at this check tested one hand-picked <rect> (the one just repaired) and went
// green while 647 others were still black.
// 3. Content the Driver specified explicitly: Mark 5:1-20 in full, 12 wheel segments,
// 3. Content specified explicitly: Mark 5:1-20 in full, 12 wheel segments,
// the prayer rhythms, and exactly two minimal metacamp.org attributions.
//
// Usage: node internal/check-packet-render.mjs [path/to/packet.html]
@ -167,8 +167,8 @@ const translated = lang !== 'en'
// ── THE LARGEST BLOCK OF READER-FACING TEXT, REPORTED SEPARATELY ────────────
// point4_passage is Mark 5:1-20 in full — 2,141 characters, by far the longest string
// in the packet and the one the Driver singled out ("prayer point number four is
// actually read the word"). It is ABSENT from all ten non-English blocks, so every
// in the packet and the one singled out: prayer point four is reading the word
// itself. It is ABSENT from all ten non-English blocks, so every
// translated packet prints its longest passage in English. The per-locale "19 strings
// translated" headline looked healthy while this was true, because 19 short keys and
// one 2,141-character one count the same.
@ -202,7 +202,7 @@ const r = await page.evaluate(() => {
// gate reporting "map: palette resolves, not black — PASS, map painted", because the
// query silently retargeted to the PRAYER WHEEL's svg two sheets later. The wheel has no
// CSS variables to leave unresolved and no black fills, so both checks passed vacuously
// while page 1 — "the map one-pager", the first line of the Driver's packet spec — was
// while page 1 — the map one-pager, the first line of the packet spec — was
// blank. The sheet count stayed 8 because the empty sheet still exists.
// The packet's page order is the product spec and does not vary: sheet 0 IS the map page.
const sheet0 = document.querySelector('.sheet')
@ -369,7 +369,7 @@ const r = await page.evaluate(() => {
// "12 segments, 11/11 rhythms, content intact — PASS" on a prayer wheel that was a
// twelve-colour pie chart with no words on it. Both numbers stayed true: the
// segments are <path> elements and survive, and the words were still on the points
// page. The Driver's spec names the wheel as one of the packet's four sections.
// page. The spec names the wheel as one of the packet's four sections.
//
// COUNT ELEMENTS, NOT DISTINCT WORDS. `order` renders 12 segments from 12 keys, but
// `praise` and `praise_end` collapse to the same word in English (which is why the
@ -517,15 +517,15 @@ if (r.wheelLabels !== WHEEL_LABELS) {
+ 'and cannot see an unlabelled wheel')
}
if (r.rhythms !== r.rhythmsTotal) fails.push(`only ${r.rhythms}/${r.rhythmsTotal} prayer rhythms present`)
// Mark 5:1-20 in full is a Driver requirement for point 4: "it actually lists out the
// full scripture, because prayer point number four is actually read the word." The
// Mark 5:1-20 in full is a requirement for point 4: it lists out the full scripture,
// because prayer point four is reading the word itself. The
// English passage is 2,141 chars; 1,200 is a floor that no truncation or placeholder
// survives while leaving room for languages that render the same text more compactly
// (zh is typically ~40% shorter in characters).
const MARK5_MIN = 1200
if (r.mark5Len < MARK5_MIN) {
fails.push(r.mark5Len === 0
? 'Mark 5:1-20 is NOT printed in full (Driver requirement for point 4) — no .p4-passage block'
? 'Mark 5:1-20 is NOT printed in full (a requirement for point 4) — no .p4-passage block'
: `Mark 5:1-20 looks truncated: ${r.mark5Len} chars, expected at least ${MARK5_MIN}`)
}
// THE PRODUCT IS NAMED FOR THIS NUMBER. The page header says "12 Prayer Points" in every
@ -673,7 +673,7 @@ if (!boxes.length) {
// this assertion exists to catch SILENTLY DROPPED PEOPLE GROUPS, and a ±1 tolerance is exactly the
// slack a lost list page would hide in. So the funnel is DETECTED (.funnel-title) and added as a
// known term, leaving the list-page arithmetic as strict as it was.
// ⚠ AND THE SAME FOR THE LIFE-CALENDAR PAGES (2026-09-27, Driver's page order). Two more structural
// ⚠ AND THE SAME FOR THE LIFE-CALENDAR PAGES (2026-09-27, the specified page order). Two more structural
// sheets — a 100-year life in weeks, then the SAME grid shaded by the evangelical share of these
// groups' population — so they are DETECTED and added as known terms for exactly the reason the
// funnel was, and for the reason stated above: a tolerance here would hide a dropped list page.
@ -687,7 +687,7 @@ const expectedSheets = 1 + lifeSheets + funnelSheets + Math.ceil(r.cards / PER_P
// ── THE LIFE-CALENDAR PAGES, asserted rather than merely counted ──────────────────────────────────
// A page that EXISTS is not a page that is RIGHT: an empty grid, a grid of the wrong size, or a
// christian page whose shading silently fell to zero would all still be one sheet. 5,200 is the
// Driver's own arithmetic (100 years x 52 weeks) and is the only number here that may not drift.
// spec's own arithmetic (100 years x 52 weeks) and is the only number here that may not drift.
if (r.lifeSheets) {
const EXPECT = 5200
const perGrid = r.lifeCells / (r.lifeSheets + (r.christianSheets || 0))
@ -698,8 +698,8 @@ if (r.lifeSheets) {
}
// ⚠ AND THE SHADE MUST BE SCATTERED, NOT STACKED — the assertion that would have caught what three gates
// and a passing suite did not. rega's contact sheet (2026-09-28) showed page 3 reading as ONE SOLID BAR:
// 32 contiguous cells fill most of row 1 of 52, so the page said "a big green block" where the Driver's
// intent is "32 boxes lost in 5,200". Every assertion passed, because they all measured the COUNT.
// 32 contiguous cells fill most of row 1 of 52, so the page said "a big green block" where the
// intent is 32 boxes lost in 5,200. Every assertion passed, because they all measured the COUNT.
// Half the grid height is a deliberately loose floor: a genuine scatter of even a few cells spans most of
// the page (measured: 32 cells over 26 rows, 95% of the height), and a bar spans ~1%. Anything in between
// is worth a human look.

View file

@ -55,7 +55,7 @@ const DECLARED_ABSENT = {
// wearing more confidence. This entry shrinks as each locale's text is cited, not as a batch.
point4_passage: 'Mark 5:1-20 — awaiting a CITED public-domain translation for hi/zh/pt/it/ro',
point4_passage_credit: 'the attribution line for the passage above',
// ── The Driver's new packet pages, 2026-09-27: a 100-year life calendar, the same grid shaded by
// ── The new packet pages, 2026-09-27: a 100-year life calendar, the same grid shaded by
// the evangelical share of these groups' population, and the funnel's equation sidebar + tip.
// ENGLISH-ONLY ON PURPOSE, and declared rather than back-filled: English text copied into ten
// locale blocks would make them read as TRANSLATED to this gate and to a reader, which is worse

View file

@ -2,7 +2,7 @@
/**
* check-poster-action-print — a poster button prints unless its consumer says why it downloads. Headless.
*
* Driver rule 2026-09-28: an export-poster button opens the PRINT DIALOG with a preview, never a direct
* The rule since 2026-09-28: an export-poster button opens the PRINT DIALOG with a preview, never a direct
* download. PosterWall defaults posterAction to 'print'; this asserts that (a) the default stays 'print',
* and (b) any consumer passing posterAction 'download' has a `posterAction-download:` comment with its
* reason within the 3 lines above.

View file

@ -37,8 +37,8 @@ const LEAF = process.argv[2] || DEFAULT_LEAF
const WIDTHS = (process.env.CARD_FIT_WIDTH || '390,768,1024,1440,1920')
.split(',').map((n) => Number(n.trim())).filter(Boolean)
// ★ AND SWEEP LOCALES, for exactly the same reason the widths are swept. The Driver's requirement
// was "full words visible AND support localization"; this gate only ever ran in English, which is
// ★ AND SWEEP LOCALES, for exactly the same reason the widths are swept. The requirement was full
// words visible AND localization supported; this gate only ever ran in English, which is
// the shortest of the eleven. MEASURED 2026-09-24 at 1440px, the three print labels:
// en Prayer List 76px · Card Deck 72px · Poster Map 79px
// ro Listă de rugăciune 122px · Pachet de cartonașe 133px · Harta afișului 92px (+75%)
@ -217,13 +217,13 @@ async function measure(width, lang) {
worst = Math.max(worst, Math.round(Math.max(rb.right - b.right, b.left - rb.left)))
}
// ⚠ THE VERTICAL AXIS TOO — this measured only left/right, which is exactly how it passed a
// driver-visible regression. Fixing the horizontal overflow with flex-wrap grew the row
// visible regression. Fixing the horizontal overflow with flex-wrap grew the row
// 35px -> 78px, and because it sits at bottom:-14px the extra height pushed its TOP from
// 216px up to 173px in a 238px card: over the preview image and the card's outline.
// Horizontal clean, card visibly broken, gate green.
// COUNT THE PREVIEW, do not merely guard on it. An if-guard protects this loop from a
// placeholder card — and also means a card with NO preview passes the over-preview assertion
// perfectly. The driver reported BOTH a missing-previews symptom AND the overlap;
// perfectly. BOTH a missing-previews symptom AND the overlap were reported;
// measuring only the overlap leaves the first unasserted.
// NOTE: no backticks in this block — it lives inside a JS template literal.
const img = c.querySelector('img')
@ -252,7 +252,7 @@ async function measure(width, lang) {
worstY = Math.max(worstY, Math.round(ib.bottom - rb.top))
}
}
// ★ A STICKER STRADDLES THE EDGE IT TAPES (Driver 2026-09-25, the sticker/banner brief).
// ★ A STICKER STRADDLES THE EDGE IT TAPES (2026-09-25, the sticker/banner brief).
// The card is a BANNER displaying a map; each sticker sits centred on a banner edge, part
// on the banner's own white space and part overhanging. Three things were each true while
// this gate was green, so all three are now asserted:
@ -264,8 +264,8 @@ async function measure(width, lang) {
if (frame) {
const fb = frame.getBoundingClientRect()
// STRADDLE: some of the sticker on the banner, some off it. Requiring a near-even split
// would encode a ratio the Driver explicitly left open ("probably an equal or more
// overlap"); requiring BOTH sides to be non-trivial is the property actually stated.
// would encode a ratio the brief explicitly left open (probably an equal or larger
// overlap); requiring BOTH sides to be non-trivial is the property actually stated.
const onBanner = Math.round(fb.bottom - rb.top)
const offBanner = Math.round(rb.bottom - fb.bottom)
if (onBanner < 4 || offBanner < 4) {
@ -274,9 +274,9 @@ async function measure(width, lang) {
worstTape = Math.abs(d) > Math.abs(worstTape) ? d : worstTape
}
}
// ★ NO NATIVE DROPDOWN IN THE DEFAULT ROW (same ruling). The Driver's reason is not
// cosmetic: "I don't want to add the dropdown for the card because it makes it hard for
// others to have dropdowns too" — a <select> here collides with the host page's own
// ★ NO NATIVE DROPDOWN IN THE DEFAULT ROW (same ruling). The reason is not cosmetic: a
// dropdown on the card makes it hard for others to have dropdowns too — a <select> here
// collides with the host page's own
// selects. The size picker is a disclosure, revealed by the Card Deck button. A <select>
// reappearing in .pfs is the regression this catches.
if (row.querySelector('select')) selectsInRow++
@ -331,14 +331,14 @@ async function measure(width, lang) {
// the text did not fit its box.
const clipped = ctrls.filter((e) => e.scrollWidth > e.clientWidth + 1)
// ★ DENSITY: how many posters share a row, and how wide each is.
// The Driver asked for "around 4 per row on desktop, like Instagram". The grid is ONE rule for
// The ask: around 4 per row on desktop, like Instagram. The grid is ONE rule for
// every width — repeat(auto-fill, minmax(210px,1fr)) — so the count falls out of the viewport
// rather than being chosen, and NOTHING measured it. The card's own verify says it: "perRow is
// currently printed nowhere and asserted nowhere. A number printed but not asserted on is
// decoration."
// ⚠ REPORTED, NOT YET ASSERTED, and that is deliberate. The desktop target is settled (~4) but
// the MOBILE one is not: 3 per row at 390px is a ~120px card, which cannot hold the wrapped
// print footer this same gate asserts is readable. Asserting a number the Driver has not
// print footer this same gate asserts is readable. Asserting a number no one has
// ruled on would encode my taste as a requirement. Measuring it first is what makes the
// ruling answerable — and it is the precondition for tuning, since you cannot tune toward a
// target you are not measuring.
@ -348,7 +348,7 @@ async function measure(width, lang) {
// row sizes [5,5,5,3,3,2,2,1,1] — the short rows are the REMAINDERS of collapsible sections,
// not a density failure. Reading the first row alone would be right here by luck and wrong on
// a wall whose first section holds one poster. The max across all rows is the density the
// Driver's "around 4 per row" is about; cardW is reported beside it because the two move
// "around 4 per row" ask is about; cardW is reported beside it because the two move
// together and the width is what explains the count.
let perRow = 0, cardW = 0
if (cards.length) {
@ -420,11 +420,11 @@ for (const r of results) {
+ '(scrollWidth exceeds clientWidth) — the button is big enough to press and no longer says '
+ 'what it does')
}
// The row belongs BELOW the preview. Driver-reported 2026-09-24: "the download print options are
// not showing up under the blue outline, they are overlapping where the blue outline would be".
// The row belongs BELOW the preview. Reported 2026-09-24: the download/print options were not
// showing under the blue outline; they overlapped where the blue outline would be.
// THE PREVIEW MUST ACTUALLY BE THERE. A card with no image passes every geometry assertion
// above trivially — nothing can overlap what does not render. Driver 2026-09-24: "it is supposed
// to show the previews".
// above trivially — nothing can overlap what does not render. The wall is
// supposed to show the previews (2026-09-24).
if (r.unnamedPreview) {
fails.push(`${at}: ${r.unnamedPreview} poster preview(s) have no alt text — the card's own text `
+ 'is a count and an action ("116 UPG · Show on map"), so the alt is the ONLY thing naming '
@ -446,8 +446,8 @@ for (const r of results) {
+ '"slight overlap over the banner, but not over the content of the banner". A row '
+ 'floating clear of the banner is as wrong as one covering the map.')
}
// ★ THE ONE THE DRIVER CARES ABOUT MOST. "The preview banner to showcase a map should not
// have anything overlapping the map because the map needs to be fully visible."
// ★ THE ONE THAT MATTERS MOST: nothing may overlap the map on the preview banner that showcases
// it, because the map needs to be fully visible.
if (r.mapCovered) {
fails.push(`${at}: ${r.mapCovered} sticker(s) OVERLAP the map preview (worst ${r.worstCover}px). `
+ 'The banner\'s header and footer stickers straddle the banner\'s edges; the map inside is '

View file

@ -2,8 +2,8 @@
//
// ★ THE POINT: 378 posters ship as .webp — 41 MB in every clone, delivered to every
// reader whether or not they open one, and a publish was ALREADY rejected once at a
// 74 MB asset tree. Driver: "it would be a big win to not have to have 80mb of
// images in a public folder and let the posters get generated by the website."
// 74 MB asset tree. Not shipping ~80 MB of images in a public folder, and letting
// the website generate the posters, is a big win.
//
// The fallback rasterises posters/<id>.html in the browser. This gate proves the
// generated image is REAL, because the failure mode here is silent and total:

View file

@ -3,8 +3,8 @@
* check-prayer-walk-planner.mjs — the Prayer Walk Planner must be a full-page tool that prints a
* real walking map.
*
* WHAT THIS PROTECTS (the Driver: "the prayer walk planner is still a very tiny map, not expanding
* the full page", and "the printed map is a ~10px sliver"):
* WHAT THIS PROTECTS (as reported: the prayer walk planner was still a very tiny map that did not
* fill the page, and the printed map was a ~10px sliver):
* 1. SIZE — the map canvas fills the page below the toolbar, standalone (its leaf) AND inside the
* Research Gallery, at 1280x800 and 390x844. Measured before the fix: 747x76 in the gallery on
* desktop and 390x0 on a phone, while the leaf filled — the gallery's inline host style fell
@ -104,7 +104,7 @@ try {
// 5. PRINT RESOLUTION, MEASURED: the real printed sheet laid out as PRINT at the printable width
// (8.5in - 2 x 12mm @page margins = 725 CSS px; emulateMedia alone does NOT apply @page margins,
// which is how a first measurement read the frame as 8.48in wide). DPI = natural px / printed in.
// The Driver: "a print that stays sharp when zoomed" — at >= 300 DPI a zoomed PDF shows real
// The ask: a print that stays sharp when zoomed — at >= 300 DPI a zoomed PDF shows real
// street names and house outlines, not an upscaled screen grab.
await p.evaluate(() => { new MutationObserver(ms => { for (const m of ms) for (const n of m.addedNodes) if (n.tagName === 'IFRAME') { try { n.contentWindow.print = () => {} } catch (_) {} window.__pwpFrame = n } }).observe(document.body, { childList: true }) })
await p.evaluate(`(async () => { const el = (${findEl.toString()})(); const z = el.listPolygons().find(x => x.title === 'Balat'); el.selectPolygon(z.id, true); await el.print() })()`)

View file

@ -1,10 +1,9 @@
// check-print-geometry — the THREE print paths must each produce their OWN page
// geometry, from the UPG-100 page, in JavaScript.
//
// Driver: "make sure poster generation works and the aspect ratio of the print
// dialog can be custom based on your print point, so print dialog works for
// poster, for vertical packet, and for horizontal cards... not using python but
// all working in the jamstack maps."
// THE ASK: poster generation works, and the print dialog's aspect ratio follows
// what is being printed, so the print dialog works for the poster, the vertical
// packet and the horizontal cards, all in the JAMstack maps rather than in Python.
//
// One gate over all three because the failure they share is the same: a print
// path silently falling back to letter. Chromium does that SILENTLY for any

View file

@ -3,7 +3,7 @@
* check-provide-shadowing — a nested custom element keeps its OWN pinia, i18n and profileModules, and
* says so deliberately instead of tripping Vue's overwrite warning 24 times.
*
* THE DEFECT (Driver's console, 2026-09-28): 24 Vue warnings —
* THE DEFECT (seen in a live console, 2026-09-28): 24 Vue warnings —
* 'App already provides property with key "profileModules" / "appI18n" / "Symbol(pinia)" inherited
* from its parent element. It will be overwritten with the new value.'
* eight of each, from a composite mounting child custom elements.

View file

@ -5,8 +5,8 @@
* ★ THE DEFECT. The country posters shipped rendering the UPG-100 packet: the kicker "UNENGAGED
* PEOPLE GROUPS", the line "This packet is 1 of {n} UPG-100 lists", the whole Project 2000/7000/
* 4700/700 narrowing funnel, and a below-map block telling the reader to divide the set among
* THIRTY disciples — coherent for ~30 UPG-100 zones, incoherent for ~170 countries. Reported by
* the Driver, 2026-09-28. It was not a mislabel: NO series parameter existed anywhere, so "the
* THIRTY disciples — coherent for ~30 UPG-100 zones, incoherent for ~170 countries. Reported
* 2026-09-28. It was not a mislabel: NO series parameter existed anywhere, so "the
* packet" and "the UPG-100 packet" were one thing and nothing could disagree.
*
* ★ WHY A GATE AND NOT JUST THE FIX. Every one of those strings rendered perfectly — right size,

View file

@ -4,7 +4,7 @@
*
* Two arms, one root cause: a line's POSITION in a setup block is not the same as the MOMENT it
* runs, and both defects below shipped because the author read the file top-to-bottom and assumed
* the runtime would too. Both were found in the Driver's own console, in a gallery tab, on code
* the runtime would too. Both were found in a live console, in a gallery tab, on code
* that built clean, parsed clean and passed every other gate in this tree.
*
* ── ARM A: IMMEDIATE-TDZ ──────────────────────────────────────────────────────────────────────

View file

@ -1,9 +1,9 @@
// check-share-policy — one written rule for WHICH MAPS CARRY A SHARE BUTTON.
//
// Driver: "Some maps need a map-level share button and some do not. The Prayer
// THE RULE: some maps need a map-level share button and some do not. The Prayer
// Progress map is the one intended for embedding on other people's websites, so it
// MUST have a share button. UPG-100 must not (the header already provides it).
// Decide + record the per-map rule, then apply it."
// The per-map rule is decided and recorded here, then applied.
//
// The BEHAVIOUR already existed on both of those. What did not exist was the RULE,
// so nothing stopped the next agent adding a share button to a map that should not

View file

@ -24,7 +24,7 @@
* images before anyone shot the studio.
*
* WHY A WARNING AND NOT A HARD FAIL
* Installing a font needs a network fetch of a binary onto this box — a Driver call, not something
* Installing a font needs a network fetch of a binary onto this box — a product decision, not something
* a screenshot refresh should do quietly. Until that is decided, failing the gate would block work
* on a condition the operator cannot clear. So this REPORTS loudly and exits 0 by default; pass
* --strict (or set SHOT_FIDELITY_STRICT=1) to make it blocking once a font is installed.

View file

@ -1,9 +1,9 @@
#!/usr/bin/env node
/**
* check-spa-tab-order.mjs — the cap-5 product SPA must open on the Big Picture, in the Driver's order.
* check-spa-tab-order.mjs — the cap-5 product SPA must open on the Big Picture, in the specified order.
*
* WHAT THIS PROTECTS. The Driver specified the rail explicitly: "cap-5 tab order (Big Picture,
* UPG-100, Prayer Progress, Gospel Resources)". Tab 1 is not a preference — it is the LANDING
* WHAT THIS PROTECTS. The rail was specified explicitly: cap-5 tab order (Big Picture, UPG-100,
* Prayer Progress, Gospel Resources). Tab 1 is not a preference — it is the LANDING
* SURFACE, the first thing anyone opening doxa-maps-spa sees, and the opening step of the
* onboarding ramp. Today that ordering is held only by the order of entries in a source array and
* by comments explaining why; nothing fails if someone reorders them.
@ -18,7 +18,7 @@
* ★ IT ASSERTS THE RENDERED RAIL, NOT THE SOURCE ARRAY. Parsing NAV_MODEL is what I tried first and
* it returned an EMPTY list — the entries do not carry the `id:` shape I assumed, and a regex over
* a 300-line commented array is guesswork either way. The rail is the thing the user sees, it is
* what the Driver described, and reading it requires no assumption about how it is built. A source
* what was described, and reading it requires no assumption about how it is built. A source
* parse could also pass while a rendering bug shows a different order.
*
* ORDER, NOT MEMBERSHIP. Checking that four names are present would pass on a rail that opens on
@ -42,9 +42,9 @@ const BASE = process.env.BASE_URL || `http://127.0.0.1:4173/${TREE_NAME}`
// Measure THIS checkout, or measure nothing (exit 75 = NOT MEASURED; see _serves-this-tree.mjs).
await __treeGuard(BASE, __ROOT, 'SPA-TAB-ORDER')
// The Driver's sequence, verbatim from the open-work brief. Prefix-matched (case-insensitive,
// The specified sequence, verbatim from the open-work brief. Prefix-matched (case-insensitive,
// whitespace-collapsed) so a row may carry its subtitle.
// The Driver's four, asserted as a RELATIVE ORDER rather than as positions 1-4.
// The specified four, asserted as a RELATIVE ORDER rather than as positions 1-4.
//
// ⚠ THIS USED TO DEMAND A FOUR-ROW RAIL, and it had been FAILING ever since the rail gained its
// three-section structure (4c74bd7, 2026-09-23, "Restore three-section tab structure:
@ -52,9 +52,9 @@ await __treeGuard(BASE, __ROOT, 'SPA-TAB-ORDER')
// SEVEN rows —
// Big Picture · Simple Research · Regions · UPG-100 List · Prayer Progress ·
// Cities with Internet · Gospel Resources
// — and the Driver's four are ALL PRESENT, at 0/3/4/6, in their specified relative order. The
// — and the specified four are ALL PRESENT, at 0/3/4/6, in their specified relative order. The
// product honours the instruction; this gate was asserting a shape the instruction never named.
// It quoted the Driver correctly and then over-read it: "(Big Picture, UPG-100, Prayer Progress,
// It quoted the brief correctly and then over-read it: "(Big Picture, UPG-100, Prayer Progress,
// Gospel Resources)" names an ORDER, not a row count, and nothing there forbids a row between.
//
// So: assert what was actually specified. Tab 1 must still be Big Picture — that IS positional,
@ -114,7 +114,7 @@ const norm = (s) => s.toLowerCase().replace(/[^a-z0-9]+/g, ' ').trim()
const at = EXPECTED.map((want) => rows.findIndex((r) => norm(r).startsWith(norm(want))))
const fails = []
// 1. PRESENCE. A name the Driver specified must be in the rail at all.
// 1. PRESENCE. A specified name must be in the rail at all.
EXPECTED.forEach((want, i) => {
if (at[i] === -1) fails.push(`"${want}" is not in the rail at all`)
})
@ -129,12 +129,12 @@ if (at[0] > 0) {
for (let i = 1; i < EXPECTED.length; i++) {
if (at[i] !== -1 && at[i - 1] !== -1 && at[i] < at[i - 1]) {
fails.push(`"${EXPECTED[i]}" (row ${at[i] + 1}) comes before "${EXPECTED[i - 1]}" `
+ `(row ${at[i - 1] + 1}) — the Driver's order is reversed here`)
+ `(row ${at[i - 1] + 1}) — the specified order is reversed here`)
}
}
if (fails.length) {
console.error(`SPA-TAB-ORDER: FAIL — ${fails.length} problem(s) with the Driver's rail order.\n`)
console.error(`SPA-TAB-ORDER: FAIL — ${fails.length} problem(s) with the specified rail order.\n`)
for (const f of fails) console.error(' - ' + f)
console.error('\n Rendered rail:')
rows.forEach((r, i) => console.error(` ${i + 1}. ${r.slice(0, 52)}`))
@ -146,7 +146,7 @@ if (fails.length) {
}
console.log(`SPA-TAB-ORDER: PASS — the rail opens in the specified order (${rows.length} rows total).`)
// Print the WHOLE rendered rail and mark the Driver's four, so a reader can see the rows that
// Print the WHOLE rendered rail and mark the specified four, so a reader can see the rows that
// sit between them rather than only the names being asserted. A gate that prints just its own
// four would hide the very structure that made the old positional assertion wrong.
rows.forEach((r, i) => {

View file

@ -3,7 +3,7 @@
//
// The square map-control variant: is it the ENGINE'S card, and does nothing else move?
//
// The Driver asked for square controls as an OPTION beside the round ones. The key fact that shaped
// Square controls were asked for as an OPTION beside the round ones. The key fact that shaped
// the implementation is that the square look is not a design of DX-life's at all: their CountryMap.vue
// adds mapboxgl.NavigationControl + FullscreenControl and overrides NOTHING, so what he admired is the
// ENGINE'S DEFAULT grouped card. That makes the target published and exact rather than a matter of

View file

@ -2,11 +2,11 @@
/**
* check-studio-rail — the Plugin Studio's rail is laid out like plugin-designs/. Headless.
*
* The Driver's ask (2026-09-26): one FOLDER per BUNDLE with its ELEMENTS inside, bundles sorted by
* The ask (2026-09-26): one FOLDER per BUNDLE with its ELEMENTS inside, bundles sorted by
* capability, highest first. Runs the studio's REAL treeSections / compositionTrees / rowsFromManifest
* (loaded through Vite, no browser) against the BUILT manifest and asserts:
* - EVERY bundle sits in a capability section (they run CAP 5 → 1), in the section of its HIGHEST
* capability, once; there is no Experimental section: every plugin is capability 1-5 (the Driver,
* capability, once; there is no Experimental section: every plugin is capability 1-5 (since
* 2026-09-27), and a non-production bundle says so on its own rows;
* - bundles within a section are in name order;
* - a bundle with several elements is ONE folder holding all of them; a single-element bundle is its element;
@ -129,7 +129,7 @@ try {
}
}
for (const b of byBundle.keys()) if (!seen.has(b)) fail(`bundle ${b} is missing from the rail`)
// A row is NAMED, never shown as a bare id (the Driver: Gospel x Internet read just "board").
// A row is NAMED, never shown as a bare id (Gospel x Internet read just "board").
for (const r of ctx.rows) if (r.label === r.profile || r.label === r.bundle) fail(`${r.key} shows a bare id ("${r.label}") — give its <profile>.meta.json a "label"`)
// ★ AND A PRODUCTION PROFILE MUST DECLARE ITS NAME, not inherit one.
//

View file

@ -7,7 +7,7 @@
* row — under a real instruction to imitate the YouTube active row. Measured against the rest of
* the product that is the only thing shaped that way: .vtl-collapse is 8px, the filter toggles in
* library/components/search are 8px, and 8px is already the most common radius in the layout file.
* The Driver reversed it: "there is nowhere else in the app that has that same roundedness."
* That was reversed: nothing else in the app has that same roundedness.
*
* ★ WHY A GATE AND NOT JUST THE FIX. The 999px rationale is still in the file, kept deliberately as
* history. A superseded rationale that still reads as current is exactly how a reversed decision
@ -17,7 +17,7 @@
*
* WHAT IT ASSERTS, and why each assertion is here rather than the obvious one:
* A. .vtl-tab declares its radius through var(--vtl-radius,…), not a literal. Asserting the
* TOKEN rather than the value "8px" is the point: the Driver's requirement is that the tabs,
* TOKEN rather than the value "8px" is the point: the requirement is that the tabs,
* the collapse button and the filter toggles agree, which a shared knob guarantees and three
* matching literals only coincidentally satisfy.
* B. The token's fallback equals .vtl-collapse's literal radius, READ FROM THE FILE. A hard-coded
@ -124,7 +124,7 @@ function auditLines(lines) {
if (tok[1].trim() !== reference) {
problems.push(`${REL}:${t.line} — the token fallback is ${tok[1].trim()} but .vtl-collapse ` +
`(line ${collapse[0].line}) is ${reference}.\n` +
' These are the two surfaces the Driver named as needing to match. Change both or neither.')
' These are the two surfaces named as needing to match. Change both or neither.')
}
}

View file

@ -23,7 +23,7 @@
* mapboxToken, fed by profile-config.tk and window.MAP_TOKEN)
* TOK-LITERAL token a `pk.ey…` token literal in code (comments excluded)
* LOC-ENUS locale toLocaleString('en-US') / Intl.NumberFormat('en-US') (use the active locale).
* WARN only: whether numerals follow the locale is a Driver decision
* WARN only: whether numerals follow the locale is pending a product decision
* LIST-FETCH list a file that fetches the people-group list (/api/people-groups/list, a
* source's endpoints.bulk, or the doxa-people-groups.json roster) without
* applyPeopleGroupList: that map would show DOXA's list whatever list was asked
@ -66,7 +66,7 @@ const ALLOW = {
+ 'column warns every time a bundle adopts the shared helper -- punishing the fix and teaching '
+ 'people to stop reading the column. Restricted to library/ (see the lookup): no per-bundle source '
+ 'can be wildcarded, and a new toLocaleString(\'en-US\') inside research-dashboard or '
+ 'people-groups-grid still warns. Revisit only if the Driver unholds numerals.',
+ 'people-groups-grid still warns. Revisit only if the numerals decision changes.',
'prayer-walk-planner:ENG-PIN': 'keyless by design: always MapLibre, needs no token (index.js header)',
'*:LOC-ENUS:library/components/maps/RegionCountryMap.vue':
'HELD DECISION, not an oversight: the map popup formats population as fixed en-US on purpose. '
@ -118,7 +118,7 @@ const RULES = [
// MEASURED before changing the rule: of the NINE LOC-ENUS sites in the tree, exactly TWO are
// such fallbacks (region-dashboards.vue, regions-list.vue). The other seven are genuinely
// operative fixed en-US and are still flagged, including RegionCountryMap's popup, which is a
// HELD Driver decision with its own card rather than an oversight.
// HELD product decision with its own card rather than an oversight.
const out = []
const re = /(?:toLocaleString|NumberFormat|DateTimeFormat)\s*\(\s*['"]en(?:-US)?['"]/g
let m
@ -307,7 +307,7 @@ if (REPORT) {
}
for (const f of findings) console.log(`${f.warn ? 'WARN ' : 'BYPASS '} ${f.b.padEnd(22)} ${f.rule.padEnd(11)} ${f.where} ${f.text}`)
const hard = findings.filter((f) => !f.warn)
if (findings.length - hard.length) console.log(`UNIVERSAL-PROPS: ${findings.length - hard.length} WARN(s): LOC-ENUS fixes numerals to en-US whatever the locale. Warn-only until the Driver decides the numerals policy.`)
if (findings.length - hard.length) console.log(`UNIVERSAL-PROPS: ${findings.length - hard.length} WARN(s): LOC-ENUS fixes numerals to en-US whatever the locale. Warn-only until the numerals policy is decided.`)
if (hard.length) {
console.log(`UNIVERSAL-PROPS: FAIL — ${hard.length} bypass(es) in ${new Set(hard.map((f) => f.b)).size} bundle(s). Read the prop through the library (documentation/reference-universal-props.md), or ALLOW it here with the reason it is by design.`)
process.exit(1)

View file

@ -2,8 +2,8 @@
/**
* check-upg-map-data-painted — the UPG-100 map must have OUR DATA on it, not just a basemap.
*
* THE DEFECT THIS CATCHES. Driver 2026-09-24: "the polygons for the map and map colors and pins
* are not showing up". Nothing in the gate suite could answer that. verify-map-paint asserts a live
* THE DEFECT THIS CATCHES. Reported 2026-09-24: the map's polygons, its colours and its pins were
* not showing up. Nothing in the gate suite could answer that. verify-map-paint asserts a live
* GL context, zero console errors and that Mapbox emitted attribution — every one of which is TRUE
* of a perfectly-painted basemap carrying none of our zones, polygons or pins. "The map engine
* booted and tiles are loading" and "my data is on it" are two different properties, and only the

View file

@ -7,7 +7,7 @@
* 404 /__plugin-gallery//plugin-designs/big-picture/index.js
* ^^ a base concatenated onto an ALREADY-ROOTED path
* Every tab read "Could not load <plugin>", which looks like 29 broken plugins rather than one broken
* string join. The Driver saw it as "none of my plugins are working".
* string join. It read as none of the plugins working.
*
* ★ WHY NO EXISTING GATE COULD HAVE CAUGHT IT — this is the reason this file exists rather than a test
* inside the build. The BUILT manifest hands RELATIVE refs ("./big-picture/big-picture.js"), so

View file

@ -2,19 +2,19 @@
/**
* check-verification-tags.mjs — enforce POL-two-kinds-of-verification on the builder backlog.
*
* THE DEFECT THIS CATCHES. A card was closed `status: "done"` on an agent measurement, the Driver saw
* THE DEFECT THIS CATCHES. A card was closed `status: "done"` on an agent measurement, a human saw
* the bug still happening, and the card had NO FIELD in which that disagreement could be recorded. So
* the board said "done" and the product said "broken" and nothing in the data knew. It happened at
* least twice before the policy existed:
* - duplicate badges: closed on LEAF-page evidence while the Driver was looking at the COMPOSITE;
* - duplicate badges: closed on LEAF-page evidence while a human was looking at the COMPOSITE;
* - filter dropdown: closed on the dashboard header while five more dropdowns live in the grid.
*
* ★ THE POINT IS NOT TO REQUIRE HUMAN SIGN-OFF. The Driver explicitly rejected that — "the backlog
* would be entirely gated by humans and things would be super slow." An agent MAY close a card alone.
* ★ THE POINT IS NOT TO REQUIRE HUMAN SIGN-OFF. That was explicitly rejected: it would gate the whole
* backlog on humans and make everything slow. An agent MAY close a card alone.
* What this gate forbids is closing one ANONYMOUSLY, so that nobody can later ask "which of these did
* a human actually look at?"
*
* ★ THE ASYMMETRY THIS ENCODES (Driver's own words): human verification is more reliable on VISUAL
* ★ THE ASYMMETRY THIS ENCODES: human verification is more reliable on VISUAL
* bugs; agent verification is more reliable on NON-VISUAL bugs. A headless probe reads geometry, not
* perception — it can measure that a menu is at dTop=4 and still be blind to it feeling wrong. So a
* VISUAL card closed on agent evidence is a HYPOTHESIS; a STATE card closed on agent evidence is a
@ -72,7 +72,7 @@ const unquoted = cards.filter(
if (unquoted.length) {
errs.push(
`${unquoted.length} card(s) claim human_verified with no human_evidence.\n` +
' Only the Driver grants human verification, and the quote is what makes that checkable. Without\n' +
' Only a human grants human verification, and the quote is what makes that checkable. Without\n' +
' it, an agent can quietly promote its own agent-verification into a human one — which would erase\n' +
' the single distinction this whole policy exists to preserve.\n' +
unquoted.map((c) => ' - ' + c.id).join('\n')
@ -84,7 +84,7 @@ const wrongly = cards.filter((c) => c.fields && c.fields.verification_conflict &
if (wrongly.length) {
errs.push(
`${wrongly.length} card(s) are marked verification_conflict AND done.\n` +
' A conflict means a tool says fixed while the Driver says broken. That is not a done card; it is\n' +
' A conflict means a tool says fixed while a human says broken. That is not a done card; it is\n' +
' the strongest available signal that the probe measured the wrong surface. Reopen it.\n' +
wrongly.map((c) => ' - ' + c.id).join('\n')
)
@ -106,5 +106,5 @@ console.log(` human-confirmed .............. ${humanSeen.length}`)
console.log(` agent-only, STATE (strong) .. ${agentState.length} — counts, routing, file shapes; no human needed`)
console.log(` agent-only, VISUAL (weak) .... ${agentVisual.length} — ★ closed on geometry, never on perception`)
console.log(' The last number is the unpaid-attention ledger: cards a tool called fixed on a question')
console.log(' only an eye can settle. It is not an error — it is the list to hand the Driver when he')
console.log(' next says a bug is still there, before investigating anything from scratch.')
console.log(' only an eye can settle. It is not an error — it is the list to hand a human reviewer the')
console.log(' next time one says a bug is still there, before investigating anything from scratch.')

View file

@ -24,7 +24,7 @@
* fails on a CHANGE rather than on their existence.
* "no two zones overlap" → 2 non-bloc overlaps exist (cocos-malays, andorrans) because
* "Scattered / Diaspora" overlaps the regions it scatters
* across. Declared, pending the Driver.
* across. Declared, pending a product decision.
*
* A gate that fails on the only correct example of the thing it checks teaches people to disable it.
*

View file

@ -48,7 +48,7 @@ for (const root of roots) {
console.log(`[clean] reset ${root}`)
}
// ONE FOLDER (Driver decision, card BUILD-OUTPUT-IS-ONE-FOLDER, 2026-09-25): public/ now holds only the
// ONE FOLDER (decision, card BUILD-OUTPUT-IS-ONE-FOLDER, 2026-09-25): public/ now holds only the
// output folder + a generated README.md. These are the entries the OLD layout wrote beside the tree;
// removing them here lets a checkout that still has them converge on the new layout at its next build
// instead of carrying them forever. (public/api/ is left alone: write-token owns it, and only when a

View file

@ -1,5 +1,5 @@
{
"_": "Element census preset for check:element-census-vs-live — see internal/check-element-census-vs-live.mjs. Every sanctioned entry must carry the Driver's words, the date and the card; an entry without them is refused. The one sanctioned entry carries the Driver's words, its date and its card; everything else from the first run went to the shepherd as findings: the first run's ours-only list went to the shepherd as findings for adjudication, which is the rule, and an allowlist written by the owner to make its own gate green is not evidence.",
"_": "Element census preset for check:element-census-vs-live — see internal/check-element-census-vs-live.mjs. Every sanctioned entry must carry the ruling, the date and the card; an entry without them is refused. The one sanctioned entry carries its ruling, its date and its card; the first run's ours-only list went out as findings for adjudication, which is the rule, and an allowlist written by the owner to make its own gate green is not evidence.",
"_leaf": "THE LEAF IS THE COMPOSITE, NOT THE GRID ALONE, AND THAT IS THE WHOLE POINT OF THE PAIRING. people-groups-grid is a copy of DX-life's UupgsList.vue, which is the BODY of doxa.life/research — but that page is a MAP plus that body. MEASURED 2026-09-25: censusing the grid leaf alone against /research reports 45 omissions, and they are the research map's entire chrome (Clustering, Off/Native/MST/Network, the tab strip, the legend) — nothing to do with the grid, and a list that long trains a reader to ignore it. research-map-plus-grid is map + grid, which is what /research IS, so it is the only pairing where an omission means something. 98 strings against their 95.",
"owner": "simpleresearch",
"requireH1": false,
@ -19,7 +19,7 @@
"match": "^Hide Filters$",
"flags": "i",
"what": "our filter panel is OPEN on load, so the toggle reads 'Hide Filters' where their snapshot reads 'Show Filters' — the same control in the other state, not an extra one",
"driver": "the dropdowns don't let me select them / I don't see them working ... the toggle or filters need to be INTERESTING",
"ruling": "the dropdowns could not be selected or seen working; the toggle and filters must invite use",
"date": "2026-09-21",
"card": "BUG-FILTER-DROPDOWNS-NOT-DISCOVERABLE-DRIVER-REPORTED"
}

View file

@ -1,5 +1,5 @@
{
"_": "Element census preset for check:element-census-vs-live — see internal/check-element-census-vs-live.mjs. Every sanctioned entry must carry the Driver's words, the date and the card; an entry without them is refused.",
"_": "Element census preset for check:element-census-vs-live — see internal/check-element-census-vs-live.mjs. Every sanctioned entry must carry the ruling, the date and the card; an entry without them is refused.",
"owner": "regions",
"pages": [
{ "name": "list /regions", "leaf": "regions-dashboard/regions-list/index.html", "live": "https://doxa.life/regions" },
@ -10,25 +10,25 @@
{
"match": "^(Select view|Regions List|Dashboard|Country Posters)$", "flags": "i",
"what": "the SELECT VIEW tab strip — sanctioned addition #1",
"driver": "the tabs we add, and the what happens when you go to the dashboard tab without selecting a region or country yet",
"ruling": "the tabs added, and what the dashboard tab shows before a region or country is selected",
"date": "2026-09-24", "card": "SPEC-REGIONS-IS-A-CLONE-PLUS-EXACTLY-TWO-ADDITIONS"
},
{
"match": "^(All Regions|Pick a region or a country on the Regions List tab to narrow this dashboard\\.)$", "flags": "i",
"what": "the no-selection chooser — sanctioned addition #2",
"driver": "the tabs we add, and the what happens when you go to the dashboard tab without selecting a region or country yet",
"ruling": "the tabs added, and what the dashboard tab shows before a region or country is selected",
"date": "2026-09-24", "card": "SPEC-REGIONS-IS-A-CLONE-PLUS-EXACTLY-TWO-ADDITIONS"
},
{
"match": "^(View country posters|Hide country posters)$", "flags": "i",
"what": "the country-poster toggle — sanctioned addition #3 (regions-list.vue:55)",
"driver": "a toggle or carrot added to the bottom of each region section that says view country posters",
"ruling": "a toggle or caret at the bottom of each region section that says View country posters",
"date": "2026-09-24", "card": "BUILD-REGIONS-POSTER-DROPDOWN-REUSE-THE-SHARED-POSTERWALL"
},
{
"match": "^(Regions|Countries)$", "flags": "",
"what": "the chooser's keyboard/screen-reader dropdowns, required by the card for addition #2",
"driver": "the tabs we add, and the what happens when you go to the dashboard tab without selecting a region or country yet",
"ruling": "the tabs added, and what the dashboard tab shows before a region or country is selected",
"date": "2026-09-24", "card": "BUILD-REGIONS-NO-SELECTION-PICK-FROM-THE-MAP"
}
]

View file

@ -646,9 +646,9 @@ ${dev ? LADDER_JS : ''}</body>
// ── CLI: the ARCHIVED gallery, public/__gallery/index.html ──────────────────────────────────
//
// ★ THE ROOT PAGE IS THE STUDIO NOW; THE GALLERY IS ARCHIVED BESIDE IT (2026-09-22).
// DRIVER: "We need to back up and ARCHIVE the stuff that is old. Instead of editing it, just back
// it up and archive it... We just need a functional plugin studio with all our plugins working. We
// do not need these old staging index.html that get in the way of everything."
// THE DIRECTION: back up and ARCHIVE what is old instead of editing it. What is needed is a
// functional Plugin Studio with every plugin working, not the old staging index.html pages that
// get in the way.
//
// The BUILT tree now behaves exactly like the DEV server (internal/plugin-gallery/dev-plugin.mjs,
// which made this move first): `/` mounts <plugin-studio>, and the gallery stays reachable at the
@ -676,7 +676,7 @@ function main() {
const nProfiles = baked.reduce((n, b) => n + b.p.length, 0)
const nInstances = baked.reduce((n, b) => n + b.p.reduce((k, p) => k + (p.in ? p.in.length : 0), 0), 0)
const nOverrides = baked.reduce((n, b) => n + b.p.reduce((k, p) => k + (p.em ? 1 : 0) + (p.in || []).filter(x => x.em).length, 0), 0)
// ARCHIVED OUT OF public/ (Driver decision, card BUILD-OUTPUT-IS-ONE-FOLDER): public/ holds only the
// ARCHIVED OUT OF public/ (decision, card BUILD-OUTPUT-IS-ONE-FOLDER): public/ holds only the
// output folder + README.md, and the studio at <tree>/index.html is the front page. The last
// generated copy is kept in the umbrella's _qa-cycle/archive/. This still renders on request, into a
// directory you name — never into public/.

View file

@ -669,8 +669,8 @@ function rootHtml(bundles, manifest) {
/**
* THE WEB-ROOT PAGE — a thin host that mounts <plugin-studio> and nothing else.
*
* DRIVER DIRECTION 2026-09-22: "We just need a functional plugin studio with all our plugins
* working. We do not need these old staging index.html that get in the way of everything."
* THE DIRECTION (2026-09-22): a functional Plugin Studio with every plugin working, and none of
* the old staging index.html pages that get in the way.
* The dev server made this move first (internal/plugin-gallery/dev-plugin.mjs); this is the same
* page for the BUILT tree, so `/` is the studio in BOTH trees and a developer who learns one
* does not meet a different product in the other.
@ -709,7 +709,7 @@ function rootHtml(bundles, manifest) {
* "the plugin works"; a mounted element with no data is the failure that looks like success.
*/
function studioRootHtml(bakedToken, integrity = '') {
// THE TREE'S FRONT PAGE (<tree>/index.html) — Driver decision, card
// THE TREE'S FRONT PAGE (<tree>/index.html) — the decision in card
// BUILD-PLUGIN-STUDIO-IS-THE-OUTPUT-FRONT-PAGE: the output is ONE folder, and opening it opens the
// studio. It used to sit one level ABOVE the tree (public/index.html) with the staging-forge chooser
// at <tree>/index.html; both are replaced by this. Relative paths, so the folder stays
@ -1525,8 +1525,8 @@ function writeManifestAndHtml(root, bundles, tokenUrlExpr, bakedToken) {
// their URL, mounted NO element, and returned a confident HTTP 200.
//
// ★ WHY THAT 200 WAS EXPENSIVE, not merely useless: probing a chooser is INDISTINGUISHABLE
// from probing a broken feature. It cost the Driver a session ("none of the elements are
// working" — they were; the page was a chooser presenting itself as the product) and cost an
// from probing a broken feature. It cost a whole session (none of the elements seemed to
// work — they did; the page was a chooser presenting itself as the product) and cost an
// agent a false "the grid is completely inert" diagnosis. A page that answers 200 with no
// element is a lying instrument, and deleting it removes a whole class of wrong answer.
//
@ -1589,7 +1589,7 @@ function main() {
// They must be COPIED here or a clean build drops them and the wall falls back
// to text placeholders while the map fails to resolve its boundaries.
{
// STAGED IN A TEMP DIRECTORY, NOT public/assets (Driver decision, card BUILD-OUTPUT-IS-ONE-FOLDER:
// STAGED IN A TEMP DIRECTORY, NOT public/assets (decision, card BUILD-OUTPUT-IS-ONE-FOLDER:
// public/ holds only the output folder + README.md). This step still gathers the sources
// (data/, plugin previews, the upg-100 boundaries) into one pool and copies the fetched
// allowlist into <tree>/assets below; the pool itself is a build intermediate. The web-root

View file

@ -21,7 +21,7 @@
* Egypt). Naming it "Western Asia" in ten locales would silently redraw the region.
* · north_america_&_non-spanish_caribbean — M49 021 is "Northern America", which excludes the
* Caribbean this bucket exists to include.
* Both stay English until the Driver rules on a translation source. An English label a reader can
* Both stay English, provisional, pending a product decision on a translation source. An English label a reader can
* still match to the map beats a localized label that names the wrong territory.
*
* ★ AND ONE SCOPE NOTE ON 419: CLDR's long name is "Latin America" while our label says

View file

@ -18,7 +18,7 @@ function __walk(sel) {
};
visit(document); return out;
}
// THE MEASUREMENT: the map's own bubble source — the DOTS the Driver looks at — plus the paint
// THE MEASUREMENT: the map's own bubble source — the DOTS a reader looks at — plus the paint
// property that decides what each one LOOKS like.
// ★ THE MAP HANDLE CAN GO STALE. The panel destroys and re-creates its Map on a re-render (and
// the dev server's HMR does this freely), which leaves window.__rdashMap pointing at a torn-down

View file

@ -22,7 +22,7 @@
* "is Mapbox rendering at idle?" number, no handle to the map object needed;
* and, on Chromium, a raw CDP session that asks the page's PerformanceMonitor for the same
* violations DevTools prints (Log.startViolationsReport: recurringHandler ≥ 50 ms) and
* captures every "[Violation] … requestAnimationFrame …" entry — the Driver's exact
* captures every "[Violation] … requestAnimationFrame …" entry — the exact
* complaint, counted. Phases: 'boot' (first canvas → +--boot ms), 'idle' (--idle ms, no
* input), 'interaction' (--interact ms: two mouse drags across the canvas, wheel zoom in
* and out, a third drag, then clicks on the first 2 visible legend rows `.stl-rows

View file

@ -7,7 +7,7 @@
* Prints, per map: median time to the bundle, to the engine script (mapbox-gl.js), to the map's
* `load` event, and the serial chain — the requests that had to finish before the next could start.
*
* ★ WHY. The Driver: "Vue needs to be bundled inline into the maps so map load time is faster." Vue is
* ★ WHY. The ask: bundle Vue inline into the maps so they load faster. Vue is
* already compiled into every IIFE (vite.config.js externalises only jspdf), so the measurement has
* to find what DOES wait on what. Every run is a FRESH browser context (cold HTTP cache) under
* Chrome DevTools network throttling, so a local server is not mistaken for a real connection.

View file

@ -1,6 +1,6 @@
#!/usr/bin/env node
/**
* mobile-sheet-probe.mjs — REPRODUCE, with numbers, the two driver-reported mobile bugs
* mobile-sheet-probe.mjs — REPRODUCE, with numbers, the two reported mobile bugs
* of the legend bottom sheet (library/components/legends/LegendMobile.vue), on a phone
* viewport, through the real touch pipeline:
*

View file

@ -84,9 +84,9 @@ export function galleryModel(reg) {
// THIS USED TO BE `'/#/' + parts.join('/')` — a hash route into the gallery PAGE. That was
// coherent while `/` WAS the gallery, but the studio nests map profiles in a lazy iframe pointed
// at exactly this URL (plugin-studio.vue:25, mode 'iframe'), so every map tab was iframing the
// gallery. The Driver saw the staging blurb — "One shared core, one build, many paste-anywhere
// maps..." — rendered INSIDE each plugin pane, and reported that the studio "is still loading
// HTMLs for each plugin with this text." It was: the gallery, recursively, once per map tab.
// gallery. The staging blurb — "One shared core, one build, many paste-anywhere maps..." —
// rendered INSIDE each plugin pane, and the studio was reported as still loading an HTML page
// with that text for each plugin. It was: the gallery, recursively, once per map tab.
//
// `/__leaf/<bundle>/<profile>[/<instance>]` instead serves a minimal page that mounts ONE element
// and nothing else — the dev equivalent of the built tree's <bundle>/<profile>/index.html, which
@ -134,7 +134,7 @@ export function galleryModel(reg) {
})
// COMPOSITION EDGES, the same source and shape generate-tree publishes: the union of every
// profile's <profile>.meta.json `composes`, at bundle level, omitted when none. This model used
// to drop them, so under the dev server — which is what the Driver's laptop runs — the studio's
// to drop them, so under the dev server — which is what a laptop runs — the studio's
// Composed-of panel and composition trees were silently empty for every composite.
const composes = [...new Set([...presets.keys()].flatMap((profile) => readComposes(b.bundle, profile)))]
return { bundle: b.bundle, scriptUrl, integrity: '', elements: [b.tag], stage: bundleStage(profiles), ...(bundleVisibility(profiles) === 'public' ? { visibility: 'public' } : {}), ...(composes.length ? { composes } : {}), profiles }
@ -151,9 +151,9 @@ export function galleryModel(reg) {
// production out of public/<treeName>/. The dev server had NO route for that shape; it served its
// leaf stand-in at `/__leaf/<bundle>/<profile>[/<instance>]` only.
//
// DRIVER-REPORTED 2026-09-23: he copied an embed URL and opened it against his dev server —
// REPORTED 2026-09-23: an embed URL, copied and opened against a dev server —
// http://localhost:5174/doxa-simple-map/doxa-simple-map/prayer/index.html
// — and it "did not work". The leaf is real (that exact path serves 200 from the built tree) and
// — did not work. The leaf is real (that exact path serves 200 from the built tree) and
// the embed is healthy (it mounts at 700px in a real iframe). The dev server simply had no route.
//
// ★ AND IT FAILED SILENTLY, which is the part that costs hours. With no route, Vite's SPA fallback
@ -171,7 +171,7 @@ export function galleryModel(reg) {
// /doxa-simple-map/doxa-simple-map/prayer/index.html bare, tree-relative — what the
// manifest's `./<b>/<p>/…` snippet
// becomes when pasted at the root
// (this is the URL the Driver used)
// (this is the URL that was used)
// /doxa-maps/doxa-simple-map/doxa-simple-map/prayer/index.html prefixed — the built tree's own
// path under public/<treeName>/
// `treeName` comes from the build config (`name`, default 'doxa-maps'), never hardcoded, so renaming
@ -385,8 +385,8 @@ export function autoSpaDevPlugin(token = '') {
// The BUILT tree makes exactly this distinction (internal/generate-tree.mjs, `scrollPage`)
// and this dev leaf did NOT — it served the fixed-viewport style to EVERY bundle. That is
// the whole of BUG-CANNOT-SCROLL-DOWN-TO-THE-GRID-UNDER-THE-DASHBOARD: the three-file fix
// for BUG-DASHBOARD-CANT-SCROLL-DOWN landed in the built tree only, so the dev URL the
// Driver actually tests on was never covered and still traps the composite.
// for BUG-DASHBOARD-CANT-SCROLL-DOWN landed in the built tree only, so the dev URL that
// actually gets tested was never covered and still traps the composite.
//
// WHY `height:100%` ON BODY IS THE TRAP, and why scrollHeight LIES about it. A definite
// height + `overflow:visible` lets content SPILL (so scrollHeight inflates — measured 3531
@ -423,15 +423,14 @@ export function autoSpaDevPlugin(token = '') {
].filter(Boolean).join('\n'))
}
// ── `/` MOUNTS THE PLUGIN STUDIO, NOT THE STAGING GALLERY ────────────────────────────
// DRIVER DIRECTION 2026-09-22: "when I run dev server on the dx maps it should just open up
// a plugin studio that gives you access to all the plugins in one place, without a lot of
// text garb that takes up so much space" — and, separately, "we do not need the extra
// generated staging environment because the plugin studio is replacing these index html."
// THE DIRECTION (2026-09-22): the dev server opens a Plugin Studio that gives access to all
// the plugins in one place, without text that takes up space; and the generated staging
// pages are no longer needed, because the Plugin Studio replaces those index.html files.
//
// WHY THIS ROUTE HAD TO EXIST AT ALL. The dev server previously served ONLY the gallery at
// `/`, and had no route for a bundle leaf — so `bun run dev` could not show the cap-5 studio
// by any URL. The Driver opened `/`, saw plugins wrapped in per-bundle frames behind a wall
// of explanatory prose, and reasonably concluded "none of the elements are working". They
// by any URL. Opening `/` showed plugins wrapped in per-bundle frames behind a wall of
// explanatory prose, which reasonably read as none of the elements working. They
// were working; the page was a CHOOSER presenting itself as the product.
//
// WHY MOUNT THE TAG RATHER THAN REDIRECT TO A LEAF: plugin-studio is a real cap-5

View file

@ -5,7 +5,7 @@
*
* WHY: the website is private on Forgejo today; the day it goes to GitHub the served folder
* must carry no internal data — no local paths, no internal repo names, no tokens, no source
* maps, no docs, no preview/staging surfaces (Driver rule, 2026-09-16). "Looks clean" is not a
* maps, no docs, no preview/staging surfaces (the rule since 2026-09-16). "Looks clean" is not a
* gate; this scan is, and it can FAIL and say why.
*
* WHAT IT CHECKS (each finding prints file + rule):
@ -63,7 +63,7 @@ const LEAK = [
[/\/home\/[a-z0-9_-]+\//i, 'local path (/home/…)'], [/\/Users\/[A-Za-z0-9_-]+\//, 'local path (/Users/…)'], [/[A-Z]:\\Users\\/, 'local path (C:\\Users\\…)'],
[/\beco-dx\b/, 'internal folder name (eco-dx)'], [/qa-cycle/, 'internal folder name (qa-cycle)'], [/\b_archive\b/, 'internal folder name (_archive)'],
[/localhost:(5174|3001|3033)\b/, 'dev-server URL'],
// ★ HARD in every mode (Driver-critical, 2026-09-24): field-access / visa data must never
// ★ HARD in every mode (critical, 2026-09-24): field-access / visa data must never
// ship in ANY public tree -- it maps how workers enter closed countries. Found live-served
// before this rule existed. Filename AND content patterns, no strict-tier softening.
[/field-access-visas/, 'PRIVATE data-class: field-access/visa table (private tables live outside the repo, see DOXA_PRIVATE_DIR)'],
@ -85,8 +85,8 @@ const LEAK_STRICT = [
[/\bpersecution\b/, 'operational-context vocabulary (allowed only inside data CSVs)'],
[/\.forgejo\/workflows/, 'CI workflow path — process, never ships in a public tree'],
]
// ★ THE PRAYER, WORD FOR WORD (Driver decision 2026-09-28): "Persecution happens globally, so the prayer is
// for the globe, and the packets carry the same prayer for all packets." The operational-vocabulary rule
// ★ THE PRAYER, WORD FOR WORD (decided 2026-09-28): persecution happens globally, so the prayer is for
// the globe, and every packet carries the same prayer. The operational-vocabulary rule
// skips EXACTLY that prayer: the text at packet-strings.json <locale>.points[0].text (the packets' prayer
// point 1, which UPG-100's Two-Cross packet shows too), read from its source in every locale, removed from
// a scanned file before the rule looks at it. Scoped by file and key, never by the word: the same word
@ -113,8 +113,8 @@ const withoutPrayer = (t) => PRAYER_ALLOWED.reduce((acc, s) => acc.split(s).join
// passed as "clean — 0 findings".
// Each entry identifies a KNOWN credential FORMAT rather than guessing from entropy: an
// entropy rule over this tree would fire on every SRI hash and base64 image and be ignored.
// ★ pk. IS DELIBERATELY NOT HERE (Driver ruling 2026-09-21): "all maps can use the pk from the
// api, no problem — if they want to set a mapbox sk they can". A Mapbox `pk.` is a PUBLIC key: it
// ★ pk. IS DELIBERATELY NOT HERE (ruled 2026-09-21): every map may use the pk from the API, and a
// host that wants to set a Mapbox sk can. A Mapbox `pk.` is a PUBLIC key: it
// is designed to ship in client code and is protected by URL RESTRICTIONS, not by secrecy
// (measured: the same pk. serves tiles 200 with a doxa.life Referer and 403s with the live test host's Referer).
// Flagging it made the token ladder's rung-3 baked token UNBUILDABLE — this gate runs inside
@ -143,8 +143,8 @@ for (const f of files) {
// NOTE: 'docs' here is the FINDING-CATEGORY name documented in this file's header, not a
// folder path. It deliberately did NOT change when docs/ was renamed to documentation/.
// The ONE exception is public/README.md: the build generates it (generate-tree publicReadme) to explain
// the folder, per the Driver's decision (BUILD-OUTPUT-IS-ONE-FOLDER: "only the output folder + a short
// README.md"). A doc anywhere else — inside the tree included — is still a finding.
// the folder, per the decision BUILD-OUTPUT-IS-ONE-FOLDER (only the output folder + a short
// README.md). A doc anywhere else — inside the tree included — is still a finding.
if ((ext === '.md' || ext === '.markdown' || ext === '.txt') && f.rel !== 'README.md') hit(findings, 'docs', f.rel, 'documentation in a served folder')
const isText = TEXT_EXT.has(ext) || TEXT_EXT.has(base)
if (!isText || f.size > 40 * 1024 * 1024) continue
@ -192,7 +192,7 @@ for (const f of files) {
}
// staging surfaces: <tree>/index.html and <tree>/<bundle>/index.html (leaf pages are deeper)
// <tree>/index.html is NOT a staging page any more: it is the Plugin Studio, the output's front page
// (Driver decision, BUILD-PLUGIN-STUDIO-IS-THE-OUTPUT-FRONT-PAGE). A <tree>/<bundle>/index.html chooser still would be.
// (decision BUILD-PLUGIN-STUDIO-IS-THE-OUTPUT-FRONT-PAGE). A <tree>/<bundle>/index.html chooser still would be.
const staging = files.filter(f => /^[^/]+\/[^/]+\/index\.html$/.test(f.rel) && f.rel.startsWith(`${TREE}/`)).map(f => f.rel)
for (const s of staging) hit(STRICT ? findings : warnings, 'staging', s, 'preview/staging page (internal surface) — exclude from a public mirror or accept knowingly')
// The drop-in contract: only the entries vite.doxa-maps-build-config.json `dropin.include` declares
@ -220,7 +220,7 @@ for (const e of rootEntries) {
if (CONTRACT.has(e)) continue
const label = files.some(f => f.rel.startsWith(`${e}/`)) ? `${e}/` : e
// ALWAYS a finding, not a strict-mode warning: public/ holding ONLY the output folder + README.md is a
// Driver decision (BUILD-OUTPUT-IS-ONE-FOLDER), not a style preference. MEASURED 2026-09-25: a rebuild
// product decision (BUILD-OUTPUT-IS-ONE-FOLDER), not a style preference. MEASURED 2026-09-25: a rebuild
// under the OLD tree name re-added public/doxa-maps/ beside public/dx-maps/, and this gate only warned.
hit(findings, 'contract', label, `outside the drop-in contract (${[...CONTRACT].join(', ')} only)`)
}

View file

@ -64,7 +64,7 @@ const BASE = `http://127.0.0.1:${server.address().port}`
// the build config is local and deterministic — the network path degrades silently, setting TK to
// '' when doxa.life is slow, after which every shot is taken token-limited and still written to
// disk. The fetch is kept only as a fallback for a tree whose build config has no baked token.
// ★ TOKENLESS BY DEFAULT (Driver, 2026-09-26). The engine default is now 'auto': no token means
// ★ TOKENLESS BY DEFAULT (2026-09-26). The engine default is now 'auto': no token means
// MapLibre, which is what every keyless partner embed shows. So the README pictures are taken with
// NO token, showing the look a reader gets by default. SHOTS_WITH_TOKEN=1 restores the Mapbox shots
// (the token ladder below).
@ -141,13 +141,13 @@ async function shot(name, path, {
//
// This shot harness serves from 127.0.0.1 and therefore sent no Referer at all. Setting it does
// NOT circumvent the restriction — the restriction exists so third-party sites cannot burn the
// Driver's quota, and these ARE doxa.life's own documentation images, generated by its own repo
// project's quota, and these ARE doxa.life's own documentation images, generated by its own repo
// from its own token. The header makes the request honest about who it is for; without it the
// legitimate owner was the one being blocked.
//
// Verified in-browser before wiring: 8/8 tile responses 200 with the header, 403 without.
// This unblocks BUG-README-SCREENSHOTS-SHOW-BLANK-BASEMAPS and
// BUG-FOUR-README-PROOF-IMAGES-SHIP-A-BLANK-BASEMAP without needing the Driver to change the
// BUG-FOUR-README-PROOF-IMAGES-SHIP-A-BLANK-BASEMAP without needing a change to the
// token's allow-list. That change is still worth making for rendering on the LIVE test host, which
// this does not address — a browser sets its own Referer and cannot be overridden this way.
//

View file

@ -60,7 +60,7 @@ const MIN_DIRS = Number(process.env.MIN_DIRS || 21)
// The old floor was set when generate-tree.mjs also emitted a <bundle>/index.html CHOOSER page for
// every bundle. Those 19 pages were removed deliberately (they returned HTTP 200, mounted NOTHING,
// were referenced by the manifest ZERO times, and their stylesheet was literally named
// "staging-forge" — they are the duplicate staging surface the Driver asked to archive). Removing
// "staging-forge" — they are the duplicate staging surface that was to be archived). Removing
// them legitimately drops the recursive index.html count from 63 to 44.
//
// ★ WHY THIS EDIT MATTERS MORE THAN THE NUMBER: left at 50, a CORRECT tree fails this gate forever,

View file

@ -122,7 +122,7 @@ const browser = await launch(BROWSER, { label: TOOL })
*
* So without WebGL the map never initialises, the MapToolbar that HOSTS the Share button is never
* rendered, and the suite blames the product for an engine limitation. Rather than silently
* defaulting away from the Driver's house rule ("never Google Chrome"), this ABORTS with the
* defaulting away from the house rule (never Google Chrome), this ABORTS with the
* measurement and the exact command that works. A suite that cannot render a map must not report
* on map sharing at all — that is the difference between a failure and a false failure.
*/
@ -197,7 +197,7 @@ async function pollCanvas(page, wait) {
* ★ THE SHARE ROUND TRIP REMAINS HONESTLY UNTESTABLE WHILE THE ENDPOINT IS DOWN. The share URL
* deliberately strips query params — a share link must be pasteable — so the reopened page has no
* ?tk= and cannot mount. That is not something a test harness may paper over: the only real fix is
* the token endpoint the Driver is gated on. Documented rather than worked around.
* the token endpoint, which is provisional, pending a product decision. Documented rather than worked around.
*
* Unset, behaviour is unchanged — a real token-plumbing break still shows up.
* MAPBOX_TOKEN=$(curl -s <endpoint> | jq -r .token) node internal/share-roundtrip-smoke.mjs …

View file

@ -141,11 +141,11 @@ if (state.tabs === 0) {
// ── assertion 5 (setup): activate ONE tab ────────────────────────────────────
// ★ SAMPLE SEVERAL TABS, AND MEASURE DEPTH — both of these were weaknesses in the first version of
// this gate, and both sat exactly where the Driver's reports live.
// this gate, and both sat exactly where the reported bugs live.
//
// (a) TAB 0 ONLY WAS NOT ENOUGH. The original activated the first tab on the theory that one mount
// exercises the whole resolver path. True for a RESOLVER bug — every tab fails together — but
// false for a per-plugin failure. The Driver reported "could not load Big Picture" while tab 0
// false for a per-plugin failure. Big Picture was reported as not loading while tab 0
// was fine, and this gate would have passed. Sampling a spread catches a single bad bundle
// without paying for all 29 (~75s). SAMPLE_TABS stays small so the budget holds.
//

View file

@ -2,7 +2,7 @@
/**
* sync-app-shells.mjs — point the native app shells at the OUTPUT FOLDER, from the tree's name.
*
* public/ holds only the output folder (Driver decision, card BUILD-OUTPUT-IS-ONE-FOLDER), so the app
* public/ holds only the output folder (decision, card BUILD-OUTPUT-IS-ONE-FOLDER), so the app
* shells load THAT folder as their web root:
* capacitor.config.json "webDir": "public/<tree>"
* `cap copy` hard-fails unless webDir holds an index.html; the tree's front page is its

View file

@ -10,12 +10,12 @@
* to rebuild — but it is NOT a standing check and is not expected to run again
* unless that class of bug returns.
*
* WHY: the Driver sees (a) a light→dark FLASH on load and (b) the basemap flipping
* WHY: a reader sees (a) a light→dark FLASH on load and (b) the basemap flipping
* FULLY light after a legend click / tab click. "It looks dark to me" is not
* evidence — this tool turns the basemap's brightness into NUMBERS sampled over
* time, before and after every UI action, so the bug (and the fix) is a checked
* fact on both surfaces. It needs NO handle to the `map` object hidden inside the
* shadow DOM: it only looks at pixels, exactly like the Driver does.
* shadow DOM: it only looks at pixels, exactly like a reader does.
*
* HOW: WebGL canvases cannot be read back (Mapbox does not keep the drawing buffer),
* so the probe deep-finds `canvas.mapboxgl-canvas` through every nested shadow root

View file

@ -119,7 +119,7 @@ try {
* 2. MAPBOX 403 — the shared pk. token is URL-restricted to doxa.life, so every tileset request
* 403s from any other origin. Mapbox then throws its own internal errors (`qt` at
* mapbox-gl.js) BECAUSE of those 403s, which is why one restriction produces ~20 lines.
* Tracked as a P0 the Driver must fix in the Mapbox account — not a code defect here.
* Tracked as a P0 to fix in the Mapbox account — not a code defect here.
* 3. TERMINAL — "No Mapbox access token provided": the ladder is EXHAUSTED. The map genuinely
* cannot paint, so this MUST still fail.
*

View file

@ -155,12 +155,12 @@ try {
// ★ "TOKEN PRESENT" USED TO MEAN "A STRING WAS OBTAINED", NOT "A TOKEN THAT WORKS", and the two
// differ. The only source here was https://doxa.life/api/maps/token, which serves a token
// URL-RESTRICTED to doxa.life: measured 2026-09-23, a tile request without that Referer returns 403,
// so every basemap renders blank while the pins draw fine — the exact symptom the Driver reported as
// "loading the pins, but not the actual underlying map". This banner still said "(token present)".
// so every basemap renders blank while the pins draw fine — the exact symptom reported: the pins
// loading, but not the underlying map. This banner still said "(token present)".
//
// Order matters: the BUILD CONFIG token is tried first because it is the one that works from a local
// gate (measured: config token 200, endpoint token 403, same request). It is a `pk.` public key baked
// deliberately (Driver ruling 2026-09-21), protected by URL restrictions rather than secrecy;
// deliberately (ruled 2026-09-21), protected by URL restrictions rather than secrecy;
// check:public-hygiene allows pk. and fails the build on sk./tk.. The endpoint stays as a fallback
// because a future config may legitimately carry no baked token.
//

View file

@ -249,7 +249,7 @@ for (const url of urls) {
// sized, GL is live, tiles load, attribution renders — and the pins are GONE, because an
// unstyled .mapboxgl-canvas-container computes position:STATIC instead of ABSOLUTE and every
// absolutely-positioned child loses its anchor. That is a basemap with none of our data on it,
// which is exactly what the Driver reported while this gate reported PASS.
// which is exactly what was reported while this gate reported PASS.
//
// MEASURED 2026-09-24 on upg-100-list: mapboxgl.version 3.24.0 running against a v3.3.0
// stylesheet injected into the shadow root, canvas-container position:static, 0 markers.

View file

@ -5,14 +5,14 @@
* ★ WHAT IT ASSERTS, and why each assertion exists rather than the obvious one.
*
* The obvious assertion — `scrollHeight > innerHeight` — is a FALSE PASS and was used twice to
* close this bug while the Driver could still not scroll. In the cap-5 shell the document measures
* close this bug while the page could still not be scrolled. In the cap-5 shell the document measures
* scrollHeight 900 === clientHeight 900 while the composite element measures 3380/769: the
* overflow is real, but it belongs to an element with `overflow:visible` that no scroller owns.
* Geometry said "tall"; the user's wheel moved nothing.
*
* So this gate asserts the ONLY thing the Driver actually cares about: that a REAL WHEEL GESTURE
* So this gate asserts the ONLY thing that actually matters: that a REAL WHEEL GESTURE
* over the map area moves the grid up the screen and brings it into view. It measures the grid's
* position before and after, in the SHELL, which is the surface he uses — not the standalone leaf,
* position before and after, in the SHELL, which is the surface in use — not the standalone leaf,
* which passed all along and is why the two previous fixes looked green.
*
* ★ NEGATIVE-TESTED. Reverting the fix (restoring `.ps-pane`/`.vtl-host` to `position:absolute;
@ -117,15 +117,15 @@ const run = async () => {
if (!before) { await browser.close(); fail('.pgg-root is NOT MOUNTED in the shell — different bug, not a scroll bug') }
if (before.h < 400) { await browser.close(); fail('.pgg-root mounted but collapsed (h=' + before.h + ') — not a scroll bug') }
// 2. THE DRIVER'S RULING, asserted as stated (2026-09-23, recorded in
// 2. THE RULING, asserted as stated (2026-09-23, recorded in
// plugin-designs/research-dashboard/src/components/dashboards/DashboardMapPanel.vue:~1188):
// "I can't zoom in and out on the map. The scrolling is causing that to change." →
// the map could not be zoomed because the wheel scrolled the page instead. So now
// a wheel OVER the map ZOOMS and moves the page 0 px; a wheel BELOW the map SCROLLS the page.
// ★ THIS STEP USED TO ASSERT THE OPPOSITE — that a wheel over the map must scroll the page —
// written before the ruling, when the map row filled the whole pane. Once the ruling made the
// map a bounded region with zoom always on, that assertion failed on the correct behaviour and
// kept this gate red on a product that was right (re-measured 2026-09-25: over-map 0 px, below
// the map 2502 px, grid on screen). dx-shepherd ruled the gate stale; this is the ruling.
// the map 2502 px, grid on screen). The gate was ruled stale; this is the ruling.
// Negative control: SHELL_SCROLL_NEGATIVE=1 disables scrollZoom on the map before the gestures
// (the pre-ruling behaviour), and the gate must FAIL.
const MAP = `(() => { ${WALK}
@ -169,8 +169,8 @@ const run = async () => {
console.log(' wheel BELOW map (y=' + belowY + ') : page moved ' + movedBelow + 'px (ruling: scrolls)')
console.log(' grid on screen : ' + onScreen)
if (Math.abs(movedByMap) > 4) fail('a wheel OVER the map scrolled the page ' + movedByMap + 'px. The Driver ruled (2026-09-23) that a wheel over the map ZOOMS and must not move the page — is scrollZoom disabled on this map?')
if (!(zAfter > zBefore + 0.05)) fail('a zoom-in wheel OVER the map left the zoom at ' + zAfter.toFixed(2) + ' (was ' + zBefore.toFixed(2) + '). The Driver ruled that a wheel over the map zooms it.')
if (Math.abs(movedByMap) > 4) fail('a wheel OVER the map scrolled the page ' + movedByMap + 'px. The ruling (2026-09-23): a wheel over the map ZOOMS and must not move the page — is scrollZoom disabled on this map?')
if (!(zAfter > zBefore + 0.05)) fail('a zoom-in wheel OVER the map left the zoom at ' + zAfter.toFixed(2) + ' (was ' + zBefore.toFixed(2) + '). The ruling: a wheel over the map zooms it.')
if (movedBelow < 200) fail('a wheel BELOW the map scrolled the page only ' + movedBelow + 'px. The grid is mounted at h=' + before.h + 'px and is UNREACHABLE.')
if (!onScreen) fail('the page scrolled ' + movedBelow + 'px but the grid top is still at ' + afterLow.top + ' (below the 900px fold) — still not reachable.')

View file

@ -407,7 +407,7 @@
"check:descriptions": "every shipped profile must say what it is (reads the BUILT manifest)",
"check:done-evidence": "a card may not reach status:done with NO bluf AND NO fields — the Doxa team inherits this board and an evidence-less 'done' cannot be reconciled against the code. 10 pre-existing are baselined by id; the list should only ever SHRINK.",
"check:dxlife": "⚠ NOT WAVE-SAFE, deliberately unwired: exit codes 0/1/2 are a DOCUMENTED contract (0 unchanged · 1 drift · 2 clone not found), and run-parallel grades any non-zero as FAILURE — so a checkout without the sibling /root-dx-life clone goes red on a precondition. Changing the contract could break another consumer; hand-run it, or give it a recognised SKIP first.",
"check:element-census-vs-live": "what is on ONE side and not the other, ours vs the live doxa.life page we clone: harvests every visible TEXT NODE through every shadow root on both sides and diffs the SETS. OURS-ONLY unaccounted = a scope violation (FAIL), THEIRS-ONLY = a clone omission (FAIL), SANCTIONED = an allowlist entry carrying the Driver's words + date + card (refused without them), DATA DRIFT = same label, different digits (reported). The negative control is MANDATORY: a known ours-only string must land in the ours-only set or it prints CENSUS IS BLIND and exits 75; an unreachable live page is also 75. Presets: internal/element-census/<name>.json (--preset <name>, default all); ad hoc: --leaf <bundle/profile/index.html[#hash]> --live <url> --neg <text> [--sanctioned <file>]. Promoted from regions' probe; regions passes (list 229 vs 228, the only difference its sanctioned poster toggle). Through browser-slot.sh.",
"check:element-census-vs-live": "what is on ONE side and not the other, ours vs the live doxa.life page we clone: harvests every visible TEXT NODE through every shadow root on both sides and diffs the SETS. OURS-ONLY unaccounted = a scope violation (FAIL), THEIRS-ONLY = a clone omission (FAIL), SANCTIONED = an allowlist entry carrying the ruling + date + card (refused without them), DATA DRIFT = same label, different digits (reported). The negative control is MANDATORY: a known ours-only string must land in the ours-only set or it prints CENSUS IS BLIND and exits 75; an unreachable live page is also 75. Presets: internal/element-census/<name>.json (--preset <name>, default all); ad hoc: --leaf <bundle/profile/index.html[#hash]> --live <url> --neg <text> [--sanctioned <file>]. Promoted from regions' probe; regions passes (list 229 vs 228, the only difference its sanctioned poster toggle). Through browser-slot.sh.",
"check:embed-base": "the shared URL resolver, pinned by ARITHMETIC — no browser, no network, no served tree, so it cannot SKIP itself into irrelevance. It computes what a leaf's own \"../../assets\" means and asserts the resolver agrees. Caught nothing for fifteen ticks because it did not exist: resolveAssetsBase computed one segment too high, every local gate passed (the dev web-root has assets BOTH inside and beside the tree), and the byte-level publish gates went green on the broken publish.",
"check:embed-basemap": "the gospel map's BASEMAP must paint inside a genuinely cross-origin iframe on the token the page BAKES, not one a probe forces on it. Every other browser gate here forces a restricted token (u=doxalife) that 403s on the basemap, then counts PINS — which come from our own GeoJSON and paint with no token at all — so 'the map renders' had never been measured. Runs a negative control every time by rewriting the token on the wire; if the control passes it reports PROVED NOTHING rather than a pass. ~60s, needs a browser.",
"check:embed-targets": "every embed.iframe src in manifest.json must resolve to a file that EXISTS. Those snippets ARE deployment-method 1 (iframe) — a partner pastes them verbatim — and NOTHING else asserts their targets are still emitted, because every other gate walks only files that exist and is blind to ABSENCE. `the `check:embed-targets:self-test` script` poisons a copy of the manifest and asserts the gate NOTICES: a gate that cannot fail is worse than no gate.",