From regions' triage of the headless orphans, every candidate re-run on this round's tip before
it was chained (all exit 0; the slowest, no-conflict-markers, 8.2 s):
gates:pre click-sheet coverage-consistent dashboard-filter-a11y filter-multivalue hover-channel
kit-options-reachable layout-key-namespaced legend-mirrors map-unmount-race
no-conflict-markers series-isolation snapshot-guard stale-sweep-claim
surfaces-read-the-tier translator-contacts (source readers)
gates:post embed-base link-coverage no-public-binds shared-reached tree-weight (built tree)
stale-sweep-claim reports SKIP while 1 of 1184 gap codes is undecided: it measures once the pool is
fully read, and the wave shows it as not measured until then, never as a pass.
Declared on demand instead: catalog-currency and published (they read the published host),
composite (minutes of rebuilds; landing's fresh step does this per landing), hardcoded-english
(report-only while its findings are triaged; its self-test is chained), and shot-fonts (it checks
the SCREENSHOT MACHINE's fonts, so chained it would fail a contributor's gates for their fonts).
Two proposals were declined: check:settled stays on demand (it fails inside any build by design,
and passing standalone is how it was wrongly chained before), and shot-fonts as above.
The orphan baseline shrinks 131 -> 106. gates:pre 93/0/2 of 95, gates:post 91/0/16 of 107.
check:no-orphan-gates only REPORTED scripts outside the chains, and the list grew to 87 main checks.
That day two of them hid real defects: check:i18n-never-translated, while a dead key identical in all
11 catalogues sat on development for hours, and check:auto-engine, which asserted a retired engine
rule that nothing ran. The report also matched by substring, so check:hardcoded-english counted as
run because its :self-test is in gates:pre.
The arm now decides. Every check:/verify:/smoke: script must be:
reached by exact name from gates:pre, gates:post, the browser suite's GATES or the build,
following what those scripts `bun run`;
on demand in internal/fixtures/on-demand-checks.json WITH its reason (check:settled, whose
reason this gate already documented: it fails inside any build by design);
baselined in internal/fixtures/orphan-checks-baseline.json, today's 131 orphans, for their
owners to chain, declare or delete. That list only shrinks (--shrink).
A new unchained script fails by name. A self-test runs first on every invocation. Proven two-sided on
the real tree: a planted unchained script exits 1 naming it; swapping exact matching for the old
substring test makes the self-test refuse; restored, it passes.
A browser given a 390px viewport still lays the page out at the 980px desktop
fallback unless the document declares <meta name="viewport">. Measured in
Chromium at 390x844 with isMobile:true:
without the meta: window.innerWidth = 980 (body 964px)
with the meta: window.innerWidth = 390 (body 374px)
Nothing warns. The page renders, the probe reads its numbers, and the check
goes green having measured a viewport no phone has. A full set of mobile runs
was taken this way before the layout width was ever compared to the requested
one, so the conclusions drawn from them described the wrong viewport.
Three gates already read window.innerWidth and none compares it to the width it
asked for, so all three would pass at 980 while reporting 390. This asserts the
property that makes their numbers mean what they say.
Checked on the built artifact rather than the generator template: a template
containing the right string proves nothing about the HTML that shipped. No
server needed.
width=device-width is required specifically, not just the tag: a meta carrying
only initial-scale still lays out at 980, so matching the tag name alone would
leave the gate unable to fail on its own founding bug.
The 28 sheets under assets/posters/ are exempt by their stated shape - a fixed
paper canvas (.stage{width:1500px;height:2000px}) has no mobile layout to get
wrong. Keyed on that property, not the directory, so a poster keeps its
exemption if it moves and an app page in that folder does not inherit one.
Mutation-tested against the real tree: stripping the meta from a shipped page
fails it, and replacing it with an initial-scale-only tag fails it. 8 self-tests
cover both, plus the exemption and its inverse.
check:gospel-whiteboard does not cover the adoption in the previous commit. I
poisoned GiMap into whiteboard:true — the exact regression that blanks the
production geographic map — and it still exited 0. It asserts chart geometry
(projection, radii, the bounded gap) and imports no component, so a call-site
option is invisible to it. Its pass was never evidence for this change.
check:board-map-options asserts what actually regresses: every
createWhiteboardMap call states `whiteboard` explicitly, and a geographic caller
(whiteboard:false) also states `boundaries`. A caller that inherits defaults
gets the whiteboard's, which are the inverse of a real map's.
Two-sided, because a check that cannot fail is not evidence: poisoned, it exits
1 naming GiMap.vue; restored, 0. Both negative controls run in the gate itself —
a call omitting the option must be judged non-compliant, and a compliant call
must not trip it, or the gate would just ban the helper.
Registered in all three places: script, gates:pre, //scripts. gates:pre is 77.
Lines swept on development since the capitals form joined the gate: research-dashboard, people-groups-grid, my-upg-100-list, regions-dashboard and others. --shrink drops only what is gone; nothing is added.
The suite lists a gate only after it has been run, so these waited for one. On 5b05f019e, in the
browser slot:
check:auto-engine PASS. no token -> MapLibre, 0 Mapbox requests; a host token alone ->
MapLibre, 0 Mapbox requests; ?provider=mapbox + the token -> Mapbox,
carrying it
check:engine-contract PASS. 2 providers x 3 load states: the engine asked for, or a visible
failure, never a substitute
Both entries record those results. check:engine-contract cannot demonstrate a failure at runtime
without the defect rebuilt into a bundle; its falsifiable arm is the source gate's self-test, so
check:no-silent-engine:self-test now runs in gates:post beside the gate it proves. It was chained
nowhere.
Its case 2 expected a host token alone to draw Mapbox, the rule until 2026-09-28. It was in no
chain, so it never ran against the change that made that false, and nothing else measured the case
the change is about. The cases now:
1 no token: MapLibre, zero Mapbox requests, painted, no prompt (unchanged)
2 a host token alone: still MapLibre, painted, zero Mapbox requests
3 ?provider=mapbox with that token: Mapbox, carrying that token
It joins gates:post beside check:leaf-engine-per-provider, together with check:engine-contract
(gospel's resources-map uses the engine it was asked for, or fails visibly), which was also in no
chain. Both are browser gates: NOT MEASURED outside the browser slot, measured in it.
Since 2026-09-28 resolveMapProvider returns MapLibre for 'auto' whether or not a token is present;
only an explicit provider selects Mapbox. Four comments still said "Mapbox when a token is
available": useMapProvider.js's own header (and its note on the `token` argument),
useMapInstance.js at the provider option, check-provider-switch.mjs's contract note, and the
PROVIDER note that generate-tree writes into every leaf page.
The leaf page's note now says what the page does: it finds a token first only when one is on offer,
because place search uses one on either engine, and the element decides the engine. The 64 leaf
pages are regenerated and differ only in that note. The three JS files change in comments only
(compared minified, without renaming).
The README says a plugin folder holds "an index.html for previewing it", and a fresh reader
followed that to /plugin-designs/<plugin>/index.html under `bun run dev`. leafRewrite reads any
<a>/<b>/index.html as <bundle>/<profile>, so the request reached the leaf route as bundle
"plugin-designs" and answered only {"error":"unknown bundle"}, which pointed nowhere.
The 404 now carries a hint: for a folder that is a plugin, its own page
(/__leaf/all-people-groups/all-people-groups, taken from the same registry the gallery uses);
otherwise the /__leaf/<plugin>/<profile> shape and the manifest that lists every plugin. The
status and the error stay as they were. The README now names the page as well.
studio's judgement on the nineteen REGION_OF findings: the English values are
identities, not reader text. Node ids derive from them, and every surface
that shows one translates it at render — upg-100-list.vue:440 and
poster-wall.vue:129 both read REGION_KEY[english] and call t(`poster.${…}`).
The exemption is conditional, and the condition is the whole rule. It holds
only for a name that IS a key of REGION_KEY. A region added to REGION_OF
without a matching key falls through the `|| english` arm and renders English
in all eleven locales, which is the defect upg100 already shipped once with
Americas and Europe. So the gate keeps working as the table grows rather than
blessing the file.
Deliberately not "object values" and not "legend-tree.js", as studio asked:
either would exempt the next table nobody has checked.
The parser had no newline requirement in the end, and finding out why is the
part worth keeping. The first version anchored on a closing brace at the
start of a line, so it never matched a REGION_KEY written on ONE line —
regionKeyNames returned null, the rule exited early, and BOTH two-sided cases
passed while the exemption never ran once. Poisoning the membership test
produced no failure, which is what exposed it. With the anchor gone the
poison fails, so the conditional is now proven load-bearing.
.js findings 41 -> 34.
territory-counts.js is `{ 'Andhra Pradesh': 81, 'Assam': 15, … }`, and those
keys are matched against props.shapeName off the map data. Translating one
breaks the join silently: the count goes missing and nothing errors. Same
inversion as a lookup table whose values are i18n keys, except the values are
numbers, so there is no key to resolve and that rule cannot vouch for it.
Judged on the whole object, not one property. A single numeric value proves
nothing — a map of sentences with one count in it would exempt the sentences.
Every value must be a plain non-key literal, there must be at least six of
them, and the key itself must read as a name: anything with sentence
punctuation stays loud whatever its value is.
Two refinements found by running it rather than reasoning about it. The word
limit is eight, not six, because "Dādra and Nagar Haveli and Damān and Diu"
is one real union territory and the longest key in a 46-entry table; the
punctuation test is what separates a name from a sentence, not the length.
And a bare key is now tried against every area, because legend-tree's
REGION_KEY maps a region to 'region_middle_east' and the call site reads
t(`poster.${…}`) — a suffix table is exempt on the same terms as a dotted
one: the key it names must resolve.
Four two-sided cases, and one of them exists because poisoning found the
first three did not exercise the whole rule. Removing the all-values-are-data
check left every test passing, because the rule only ever exempts KEYS and
those fixtures caught their prose as VALUES. The case that bites is a table
whose keys are names and whose values are prose: a label table, not a join
table, where the keys must still count.
With the exemption in, .js files are scanned. 220 .js files were invisible
against 142 .vue. The .js findings fall from 67 to 41.
A wall whose host passes `sheet` now generates its poster first, from the served
lib/poster-render.js, and never requests posters/ at all. The two gates for the stored-file path
predate that, and neither measured anything since:
- check:poster-print-fallback blocked only the .webp, so the generator printed and the gate saw
0 .webp and 0 .html requests: a FAIL on two served trees that said nothing about the fallback.
It now aborts lib/poster-render.js and its classic twin as well, so rasterising posters/<id>.html
is the one way through, which is what a reader gets when the generator cannot load.
- check:poster-degrades looked for a tree serving neither stored file and SKIPPED with exit 0 on
any other. The served tree ships the sources now, so it skipped every time and the skip read as
a pass. It now forces the worst case in the browser (every posters/*.webp and *.html answers 404,
the generator is aborted) and asserts the honest refusal on any tree. No wall url is NOT MEASURED.
The fallback itself is live code, not dead: it is the path whenever the generator is absent or fails.
The gate matched the role word only as `Driver`. Lines written in capitals, such as `DRIVER: "…"`,
`THE DRIVER'S …` or a `…-DRIVER-REPORTED` card id, never registered: 42 of them sat in shipped text
while the owners' sweeps, measured by this gate, read as nearly finished. research-dashboard alone
carries 24, most of them verbatim quotes.
The pattern is now `\b(?:Driver|DRIVER)\b`. A GPU or device driver, WebDriver and DRIVER_MERGE still
do not match, and the self-test now plants the capitals form as well. Three of the 42 are reworded in
this round. The other 39 join the baseline in a one-time re-measure, the only time it has grown:
15 of the 21 baselined lines remain, the 6 that repo's docs sweep removed are gone, and every added
key is a capitals-only line. It holds 54 lines in 20 files and only shrinks from here. Two of the
additions are code terms rather than citations (a "CAT-1 DRIVER CONTRACT", a "BATCH-UNION DRIVER");
their owners can reword them in their sweeps.
RegionCountryMap's note on the projection override said who ruled it out, in capitals the reference
baseline's pattern never matched; it now says what was decided and why the globe stays. The
mobile-gating header and a people-groups-grid census entry carried internal card ids that named their
source; the ids are shortened to what they describe.
RegionCountryMap.vue and check-mobile-gating.mjs change in comments only: script, template and styles
are identical before and after, compared without comments and without renaming. The census card field
is informational; nothing matches on it.
`throw new Error(`head ${n} tail` + detail)` puts a BinaryExpression between
the literal and the NewExpression, so the parent-only check saw the `+` and
not the throw, and reported a thrown message as a reader-visible leak.
Found while measuring the .js scan on legend-tree.js:392, and my first
report of it was wrong in a way worth recording: I told dx-shepherd the
developer-site exemption "is not firing on the .js path". It fires fine.
The gap is concatenation, and a .vue file concatenating inside a throw hits
it identically — the .js scan only surfaced a shape that is rarer in
templates. Three .vue findings cleared when this landed, which is the proof
it was never .js-specific: 92 -> 89.
Only `+` chains are walked. A string built with + is still the same argument
to the same call; any other expression type between the two is a different
construct and should be judged on its own site.
Two-sided: a thrown message built by concatenation goes quiet, and a
concatenated sentence that is NOT at a developer site still counts.
The .js scan itself is NOT in this commit. It needs the data-map exemption
first, or it hands two owners ~60 findings that are lookup keys they have
already classified.
(cherry picked from commit f96381481b)
The shell's comments carried 23 of the reference baseline's lines, one more written in capitals that
the baseline's pattern never saw, and four internal card ids. Each is reworded to keep the engineering
reason: the 404s, the grey pane, the scroll trap, the device rule and the curated order all stay
explained. Quotes became the rule they stated.
Comments only. The script (esbuild, whitespace and syntax only, no renaming), the template (compiled
without comments) and the styles (comments removed) are identical before and after. The baseline
shrinks by the 23 lines that are gone.
exp's last experimental seam. A print page assembled by concatenation —
'<html lang="' + locale + '" dir="' + dir + '">' — leaves the quasi '" dir="',
which is the seam between two attributes rather than between two CSS
declarations, so the seam shape added for exp does not cover it.
The cause was smaller than the symptom: dir and lang were the only standard
attributes missing from the HTML-fragment list that already holds class,
href, src, alt, loading, width, height, style, id, target and rel. They are
attribute names, not text a reader sees, and every RTL-aware print page
writes both.
Asserted on the shape rather than through a fixture, for the reason the CSS
seam commit records: this gate judges a split template literal as one string,
so no .vue fixture can produce a seam, and a fixture-based test passes
whether or not the rule exists. Removing dir|lang fails the assertion.
prayer-walk-planner.vue now has ZERO findings. The same shape in
library/packet/packet-render.js:1460 is not reachable yet — the scan walks
.vue only — and this rule will cover it when .js coverage lands.
exp found that splitting `width:${w}px;height:${h}px` leaves the quasi
"px;height:" — a seam between two declarations, carrying the tail of one
value and the head of the next property. It has a colon but no value and no
brace, so every CSS shape here missed it. Their regex is used as written.
The semicolon is what keeps prose out, and it is load-bearing: "12 people:"
has the colon but no declaration before it, and "Pools checked; nothing
found:" has both a semicolon and a colon and still counts, because the
segments either side are not property:value pairs.
Tested on the SHAPE, not through a .vue fixture, and the first version was
wrong in a way worth recording. A seam only exists as a separate string if a
quasi is judged alone, and this gate deliberately judges a split template
literal as one string — so no fixture can produce one. The fixture version I
wrote first passed with the rule DISABLED, which is exactly the vacuous green
this file exists to refuse. Asserting at the shape fails when the rule is
removed.
Measured before adding it: exp's four upg-neighbor findings and the CSS half
of prayer-walk-planner's are already gone, fixed by the two CSS shapes that
landed after their measurement. The only seam left in either bundle is an
HTML attribute one (`" dir="`), which this does not cover. So the rule
guards against a recurrence and against callers that do judge quasis
separately, rather than clearing a live count.
gospel verified the upg100 finding independently and refined the mechanism,
which changes what the rule has to look for. legendOpts re-reads t() on every
call — its own comment says the function shape is deliberate — so a reader
skimming for "a translator outside a function" would clear it. The freeze is
one level out: the result is assigned into a ref at two call sites and no
locale watcher ever re-fills them. The labels are computed freshly and then
frozen by where they are put.
So the rule now asks for a translator anywhere in the function plus a
returned structure plus a stored result, rather than requiring the call to
sit inside the returned literal. The old form happened to catch upg100's case
only because one property calls t() inline; a version building its strings
above the return would have slipped through, and that is the shape that will
recur because it looks correct at the call site.
Loosening it immediately flagged correct code in this owner's own plugin:
useDashboardShell builds its segment list with `get l() { return tStatic(…) }`
and says why in a comment — a resolved string would freeze the first locale
seen. A getter defers the read to whoever touches the property, so storing
the object is safe. Same escape hatch the provide() rule uses, for the same
reason: what matters is when the read happens.
Both new cases are in the table, and both sides of the real finding are now
proven against real code rather than fixtures: development's upg-100-list.vue
FAILS and upg100's fixed version at d533db379 PASSES, which matches the result
they got running this gate against their own slot head. Self-test 26/26.
gospel's third shape, and the one no per-string check can see. GiCombos
provides its facet config as a literal: provide('dashboardConfig', { facets:
{ access_level: { label: T('…') } } }). T() runs once at setup, the object
reaches the consumer as a plain value, and a live language switch leaves
every facet name in the boot language.
All ten call sites pass a real key and every key resolves in all eleven
locales, so "is it keyed" and "does the key exist" both answer yes. What is
wrong is WHEN the key was read. A lifetime is invisible to a check that
looks at one string at a time.
A getter or a computed inside the provide rescues it, because the read is
deferred to the consumer — which is how gospel fixed it in ecea53212.
Both sides are real rather than synthetic: the defect is still live on
development (ecea53212 is unlanded), so the gate catches it in the tree
today, and the fixed version with `get facets()` passes. Self-test 24/24.
gospel reviewed the shapes I taught this gate an hour ago and caught a real
bug in it: deciding whether a call site sits inside a reactive wrapper by
scanning the preceding 260 characters. Their GiDrawer.vue opens its
computed( at line 35 with the call sites at 95-100, and a lookback window
reported six false positives on working code — they nearly "fixed" code that
was right.
Measured on the same shape here: 2,600 characters between the watch( and the
call, ten times the window. A window cannot answer a containment question.
The check now walks backwards counting bracket depth, so it finds the wrapper
that actually encloses the offset however far above it sits, and cannot be
fooled by a closed wrapper that merely precedes it.
Two cases added, and the first is proven non-vacuous: reverting to the
lookback fails it (21/22), restoring brace depth passes (22/22). The second
is gospel's model.js:46 — a doc comment quoting a translator call, which must
not read as code.
This is the second correction to these rules in an hour. The first draft
flagged five production files by asking whether a function body merely
mentions t(); this one flagged correct code by asking what sits nearby
instead of what encloses. Both were found by someone else reading the rule
against their own plugin, which is the only reason either was caught before
landing.
The existing rule treats "inside a function" as safe, because a function
re-runs. That is true of the function and false of the label when nothing
calls the function again after the locale changes. upg100 hit both shapes
for real in their UPG-100 i18n batch.
A tree built once from t() and stored: opts() returns labels, onMounted
assigns the built tree to a ref, and no call site sits inside a watcher. The
translator call is shielded by the function, so the walk reports it clean
while the labels stay in the boot language.
A computed whose only locale source is getRuntimeLocale() or detectLocale().
Those are plain getters that notify nobody, so the computed has no reactive
dependency on the language and resolves once. It is the existing tStatic
rule generalised: the getter can be reached through any helper, not only
through a translator.
The watcher does not have to be watch(locale, …). upg100's own fix watches
() => packetLang(), which reads packetLocale.value — a real reactive source
under a local name. A signal looking for the literal `locale` would have
flagged the fix as the defect, which is the worst thing a gate can do to
someone who just did the work.
The first draft flagged five PRODUCTION files, none of them a label tree:
fetchPhotos, relabelSheet, popupHtml and two more — large functions that
call a translator once for an error string and return something unrelated.
"The body mentions t()" is not the shape. The rule now requires the
translator call to sit inside a RETURNED object or array literal, which is
what "builds a structure of labels" actually looks like.
Six cases added to the gate's own table, each bad shape paired with the good
one it must stay quiet on, plus that false positive. Self-test 20/20.
rega measured dashboard-plus-grid's own leaf at 390px: the page carried
"mobile": "desktop-only" in its profile-config, and the dashboard, the grid
and a MapLibre canvas mounted anyway. The rule lived in ProfileLoader, and
this composite defines its element without ProfileLoader (it bakes its
children in), so nothing on that page read the flag.
- library/composables/useScreenGate.js holds the rule (narrow screen, not
inside the Gallery, dismissed for the session); ProfileLoader uses it,
unchanged in behaviour.
- dashboard-plus-grid calls it: on a narrow screen the desktop-only message
replaces the element, and the dashboard and grid never mount.
- check:mobile-gating (g): every flagged profile's element must mount
through ProfileLoader (an import in its index.js, not a mention in a
comment) or call useScreenGate itself. The pre-fix composite fails it.
Every map profile's copy-paste snippet began with a preload of
api.mapbox.com's mapbox-gl.js, while its own profile-config said provider
"maplibre", and since 2026-09-28 only an explicit provider "mapbox" selects
Mapbox. So an unedited paste downloaded the Mapbox engine on every view,
never used it, and made a Mapbox request from a keyless page. Its only
guard was a stale comment asking the partner to delete the line.
- embed.script now preloads MapLibre's script and stylesheet, pinned the
way useMapInstance loads them so the preloads are reused, and its note
points a host who wants Mapbox at the variant.
- embed.scriptMapbox is that variant: the Mapbox preloads and the same
element with "provider":"mapbox".
- check:snippet-engine (gates:post, with a self-test) reads the built
manifest: no default snippet names api.mapbox.com, every map snippet
preloads MapLibre, and every Mapbox variant preloads Mapbox and sets its
provider. Planted: the old snippet and a provider-less variant both fail.
i18n-areas kept an area whenever its name showed up as any string literal
in a bundle's source graph, to cover keys assembled at runtime. But
'gospel', 'religion', 'regions', 'detail', 'poster' and others are also
ordinary ids (a colour strategy, a data source, a field, a mode), so every
bundle importing those modules carried catalogues it never shows, in all
11 locales: 80 area inclusions across 26 bundles, and gospel's catalogue
alone pushed country-posters 9.5 KB over the ceiling.
A bare name now counts only where it really builds a key: a namespace
helper (useOptionalT, useOptionalI18n, qualify) or a translator reading a
whole area (tm('regions') in useRegionsI18n is the one in use). Key
prefixes ('area.' / `area.${x}`) count as before. A bundle that assembles a
key with a leading `${…}` keeps the old generous rule, since its area
cannot be read from the source.
Since a9550858e the gate called _fileURLToPath, which it never imported
(only fileURLToPath is), so it died with a ReferenceError at load and
measured nothing — the UPG-100 legend's info popovers in es/ar/zh
included. And with no leaf URL it exited 0 as SKIP, which the wave runner
reads as a pass; it now exits 75 (NOT MEASURED), the convention the file
itself states. Swept every gate for the same undefined path aliases:
none.
A single-file build inlines every import, dynamic ones included, so
packet-strings.json (~115 KB minified: every language's prayer points,
captions and headings) rode in every bundle that can print a packet, list
or card deck. With the wall's print messages translated, country-posters
crossed the 1.5 MB ceiling (1,502,226 bytes).
The table is now served at <assets>/lib/packet-strings.json and fetched at
the first print by library/packet/packetStrings.js, the same move as the
sheet generator and ne-places.json:
- Pinned like them: PACKET_STRINGS_SHA384 in poster-render.integrity.js
(pin-poster-render.mjs); the text is hashed before it is parsed, and an
unmatched table is not used. These strings are laid into the print
document, so an unverified copy never reaches it.
- English is bundled as the fallback (a named import of the JSON, so only
the en block ships): with no asset base, on file://, on a network error or
an integrity mismatch the print comes out in English and the console says
why. A print never fails for want of this file.
- generate-tree copies it into the pool AND the served allowlist; verified
by regenerating the tree (the served copy is byte-identical to source).
- check:poster-render-pinned, two-sided: the pin matches source and served
copy, the genuine copy loads all 11 languages, a tampered copy falls back
to English (mutation-tested: disabling the hash check fails it), and an
unserved one falls back rather than failing.
- sha384 is one shared helper (library/packet/sha384.js).
Sizes: country-posters 1,378,463 (was 1,484,053), my-upg-100-list 1,143,456
(was 1,243,509), regions-dashboard 973,802 (was 1,079,448).
The legend's region sections, its Deaf row and its one-country zones, the
wall's card names, the print progress and error messages, the map's status
badge and the Population column all rendered English in every locale. None
of them was visible to check:hardcoded-english: they were single words, .js
modules, Error() arguments, library components or number formatting.
- Legend: region rows through one REGION_KEY table shared with the wall
(poster.region_*, new region_deaf); zones through zoneTitle (tier A by
country, the Deaf row by its bloc name); node ids stay English-derived.
- Wall: card labels through zoneTitle, so the Show-on-map name, alt text,
tooltip, HUD title and printed title agree with the legend and packet.
- Errors a reader sees are keyedError(key, English, params);
errorText(e, t) shows them translated, and an unkeyed error still shows
its own message. Developer detail for a failed data load goes to the
console; the badge says upgMap.mapDataFailed.
- Population: Intl compact notation in the app's locale, same precision.
- Country Posters: the unsupported card's reason is
messages.countryPosterTooMany, not the data file's English note.
- Strings only the UPG-100 map shows move to a new upgMap area, so other
bundles do not carry them (country-posters 1,490,907 bytes).
- check:hardcoded-english judges keyedError's English as a fallback
(quiet when the key resolves everywhere, a finding when it does not);
check:label-key-coverage follows REGION_KEY to legend-tree.js and covers
the legend's table and its regionFor producer. Both self-tests two-sided.
fix:sri rehashes the leaf pages but does not carry a <profile>.meta.json
change into their BASE_CONFIG, so a pinHint, a label or a mobile flag could
land in the source and stay invisible in the tree until some later landing
happened to regenerate it (PPLR's pinHint:false did exactly that).
The generator is deterministic: a clean tree regenerates to 0 changed
files, in about 3 seconds. So landing-preconditions now runs it (step 2b)
and refuses if anything under public/ changes, naming the files and the
command to run. Checked both ways: a meta.json change without the tree
regenerates one leaf page (refused); after `bun run tree` it is 0 (passes).
openPrintFrame's autoprint prints on the frame's 'load', which waits for every image, but the frame was removed 60 s after it was CREATED, loaded or not. The Wall's poster loads 939 photos (132 MB), so on a connection below about 18 Mbps the frame was gone before 'load': no print dialog, and nothing to say so. Now the frame is reclaimed 60 s after its load, at afterprint (as before), or at a 10-minute ceiling for a load that never ends. printImageBlob revoked its object URL on the same creation clock, which blanks an image that has not loaded; it now follows the frame's clock too.
New gate check:print-frame-reclaim (gates:pre), fake timers and a fake frame, so 61 s and 10 minutes take no time: a load at 61 s still finds the frame (and the object URL), 60 s after load it is reclaimed, a frame that never loads goes at the ceiling and not before, afterprint reclaims at once. Two-sided: the copy before this fix fails 6 of its 12 cases (the load-after-61-s and ceiling arms); the controls pass on both.
gospel found 12 real defects — including a search box's aria-label, which is
screen-reader text — inside 88 findings they had already described as mostly
deliberate. None were visible from the count. They classified every remaining
finding by hand and named the two shapes the gate miscounts.
A string compared with === against table data is a value, not a label.
`scope === 'Full Bible'` tests a cell of the shipped resource table; the
reader never sees that literal, and the row it selects renders through a key.
Translating it makes the comparison miss and the feature stop, while the
count falls and it looks like progress — the same inversion as a lookup-table
key, which is why gospel classed them together.
A separator followed by a brand is not a sentence. `{{ t('…') }} · GRN`
splits into a keyed half and the text node "· GRN", and only the second
reaches this scan.
The names are LISTED, not patterned. "Punctuation then a capitalised word"
would swallow "· Not found in any pool", a real caveat, and that is the class
gospel's 12 defects were hiding in. A name earns its place by being a
publisher or standard that is the same word in every language.
Each quiet case is paired in the self-test with the nearest thing that must
stay loud: a caveat after the same separator, prose that merely begins with a
brand, a real English sentence used as a table key, and an .includes()
argument. Poisoning the name list into a punctuation rule fails.
Poisoning the operator check did NOT fail, which is why there is now a
concatenation case: `greeting + 'Welcome to the prayer map'` is a
BinaryExpression too, so accepting any operator would silence a sentence
assembled for the reader. Only an equality test means "compared against
data".
Also the last finding in this owner's plugins. All seven main tabs resolve
through tabs.<id> in all eleven locales, so their English labels could never
render — a second copy of each name that could only drift. The sub-tab tables
lost theirs the same way.
Fleet 152 -> 143. This owner's plugins: 1 -> 0.
The served generator was run by importing its VERIFIED text from a blob: URL, which fails closed on a
partner page whose Content-Security-Policy forbids blob: scripts: safe, but no generated sheet there
(dx-shepherd's FIX-POSTER-RENDER-LOAD-WITHOUT-BLOB-FOR-STRICT-CSP).
The tree now also serves a CLASSIC-SCRIPT copy, lib/poster-render.global.js: the same file under a
mechanical transform (strict mode kept; the one export becomes window.DoxaPosterRender), written by
generate-tree with the function the pin is computed from (globalVariant, pin-poster-render.mjs) and on
its served allowlist — produced by regenerating the tree, not copied in by hand. posterSheets keeps the
proven path first; only when the blob import itself is refused does it load the classic copy with
<script integrity=sha384-…>, so the browser verifies it natively. An integrity FAILURE never falls back:
it throws before any import is tried.
check:poster-render-pinned adds: the classic copy matches its pin and the served copy, and — run as a
real classic script — draws the module's sheet byte for byte. The <script> path under a strict policy
needs a browser (rega). country-posters 1,473,964 bytes (ceiling 1,500,000); verify-bundles-shipped
PASS; gates:pre 74/74.
verify-bundles-shipped (every shipped bundle under 1,500,000 bytes) ran only
inside `verify`, in gates:post, and nothing in the landing called it. So a
landing that grew a bundle past the ceiling landed and then went red:
694b00991 shipped country-posters at 1,662,962 bytes. It is now step 3c of
the landing precondition, on the rebuilt tree, before the push (~0.2 s).
Its message now reports bytes: it printed "1.6MB (ceiling 1.4MB)" for
1,662,962 against 1,500,000, mixing MiB with the decimal ceiling.
A landing scan sat 10+ minutes in state S before printing step 1, with no
build under it, and the log could not say what it was waiting on. Now:
- the first line is written before any child starts (pid, node version);
- every child gets an empty stdin and a deadline (git 3 minutes, the
freshness rebuild 20), and a timeout REFUSES by name, with the elapsed
time and a pointer to main-capture diag, instead of holding the lock;
- each step prints how long the scan has run.
gates:post runs several checks at once, and a Date.now() budget measured the neighbours too: 723 ms of work failed the 3,000 ms budget once. The budget is now the process's own CPU (process.cpuUsage, user + system); the output says "CPU" and prints the wall time beside it as not budgeted.
The budget moves to 1,500 ms, near the log-midpoint of the two builds measured here: the util takes 0.5 to 0.6 s of CPU, and the pre-924a185aa all-pairs path 4.4 to 6.9 s. Each side keeps about 2.5x, so a CPU 1.5x faster or slower still tells them apart.
Two-sided, on the 8-core box under load: 24 copies at once all PASS (CPU mean 464 ms, max 625 ms, while their wall time reached 4,076 ms); the previous Date.now() check under the same load: 18 of 24 FAIL. Against the pre-924a185aa MSTClusteringUtil it FAILS: 4,418 to 6,921 ms of CPU across four runs.
Following the generator's pin (studio's follow-up): the Natural Earth table the sheets fetch at print
time (boundaries/ne-places.json) is data, not code, but pinned the same way — its sha384 sits beside the
generator's in poster-render.integrity.js (bun run pin:poster-render writes both), and posterSheets hashes
the fetched text before parsing it. A table that is not the pinned one is not used: the sheet draws no
city dots and says so in the console, rather than plotting places it cannot vouch for.
check:poster-render-pinned, two-sided for the table as for the generator: the pin matches the source
and the served copy; a FORGED table (every name replaced) yields exactly the sheet drawn with no places;
the genuine table IS used (its sheet differs) — the probe's pins now span Chad, because a single pin
framed too small an area for any place to fall inside it and the check could see nothing.
check:poster-browser-equiv still byte-identical; gates:pre green.
countries-map.vue and research-map.vue share 1,756 lines across 60
contiguous blocks, and 78% of that is one concern: the wiring between a
semantic-tree legend and the map beside it. The two files already import
the same 41 library modules and the same five legend-data composables, so
the data layer was never the duplication. The glue was, and glue is where
a fix lands in one copy and not the other: the packet kind-split, the
poster attribution reading and the packet page-1 map hand-off each shipped
fixed in one twin and broken in the other.
Only the four functions that are BYTE-IDENTICAL in both files move:
clearCountryHighlight, _flatItemsToStlNodes, onLegendRowClick and
_bridgeSelection. Measured, not assumed — applyDimFilter (185 lines against
170) and switchTab (49 against 50) genuinely differ and stay where they
are. Sharing code that merely looks similar is how a refactor picks one
plugin's behaviour for both.
A composable rather than a component: the legend markup already lives in
SemanticTreeLegend and its two shells, and a component would have forced
both plugins to give up the template where their real differences live.
Nothing is injected or reached for, so a second dashboard can adopt it
without inheriting research-map's provide tree.
Both calls sit after countryOutline and pplrInstance. The declarations they
replace were hoisted and ran above those consts; a composable call is not
hoisted, and placed too early it would read undefined at the first call
rather than at import — the legend would stop highlighting with no error.
The gate asserts both the dependency list and that ordering, for every
caller.
Rebuild-and-compare on both bundles: research-map 1,237,385 -> 1,237,709
bytes, country-posters 1,550,988 -> 1,551,317, and every behaviour-bearing
token identical in both — regions-fill, regions-border, fill-opacity,
fill-outline-color, line-color, line-width, visibility,
doxa:legend-row-click, _flat_filter, iso_3166_1_alpha_3. Counted rather
than set-compared, because minified output renames freely.
104 lines out of the plugins, shared lines 1,756 -> 1,702.
Page 1 keeps the live map rather than a fetched sheet, and it was handed
across as a bare <img> in mapSvg — raw markup the renderer interpolates.
The page-1 redesign no longer draws that full page, so the map arrived
shrunk inside a layout built for a stored sheet. It reads as a styling
quirk rather than a wiring mistake, which is why it survived here after
country-posters was fixed. The URL now goes through mapImage, which places
it as the whole page.
This is the third defect of the same shape between these two files: a fix
lands in one twin and not the other, and nothing reports it. So the gate
asserts the seam for every host that rasterises a live map, rather than for
one file.
Two fixes to the check itself, both found by poisoning it rather than by
reading it. `[^)]*` cannot cross the arrow function's own parens, so
`useMapPoster(() => map.value).render(` never matched and the new section
skipped both files — it passed while testing nothing. And an older
assertion pinned the exact literal `{ mapSvg, variant }`, so moving to the
seam failed a check that was still describing the shape it replaced; it now
asserts that the variant travels, not the argument list it travelled in. A
gate that names one spelling of a call turns every refactor into a false
alarm, and a false alarm is how a real one gets waved through.
Both twins now use the seam. The render is skipped for a card deck, which
has no map page; this map's legend menu only asks for full or lightweight,
so that arm guards a future caller rather than a path taken today.
A locale can arrive two ways. ?lang and <html lang> are read at script
evaluation, and the preload in i18n/index.js covers them. profile-config —
the channel a host page uses — reaches ProfileLoader's watchEffect and
nowhere else, which is the channel's whole purpose; the comment there
already records that an earlier document-level fix never saw it. So with
the build shipping one locale, a host-configured embed set its locale,
mirrored its direction, and then rendered English with no error anywhere.
One line beside the setRuntimeLocale that marks that path.
The RTL hold is the second half. When the catalogue is fetched rather than
inlined, nine locales get a re-render — the words change, nothing moves —
and Arabic gets a layout flip: English left to right, then the whole chrome
mirrors. So the text waits and the map does not.
Not a v-if on the profile: <component :is="ProfileComponent"> is the entire
plugin, map included, and gating it would hold the one thing that must
never wait. A data attribute on the existing root plus a scoped rule over
named chrome classes leaves every component mounted, re-enters nothing, and
lets the map paint on its own schedule. The classes are named rather than
matched by a blanket descendant rule because two of the text-bearing
components also carry map objects. visibility rather than display, so the
chrome keeps its box and nothing reflows when the text appears — a reflow at
swap time is the same defect as the flip, only quieter.
The gate asserts the seam. "ProfileLoader mentions loadLocale" would pass on
an import nothing calls, so the watchEffect's own body is read by brace
depth and the call has to sit inside it. It also refuses a hold that names
anything map-shaped, a hold applied to the whole subtree, and a fetch that
could run without a pin. Poisoned both halves: removing the call and
pointing the hold at a map container each fail with the message that names
the defect.
(cherry picked from commit 5c54750d58)
Every leaf inlines all eleven catalogues, so a reader who opens the map in
one language downloads the other ten. Measured on doxa-simple-map: the ten
unused locales are 113 KB gzipped, 40% of what a reader actually fetches.
With the flag on, the build inlines en — it is the fallbackLocale, so it
has to stay — and emits the other ten as <bundle>/locales/<lc>.json holding
exactly the areas that bundle uses. Same areasForBundle() answer the area
narrowing already uses, so the two dimensions cannot drift apart, and
emitted from the same plugin so composite-freshness, fix:sri and the
.gated/ split cover them with no new step. Off by default: the build is
byte-identical until someone sets it.
Not Vite's dynamic import. That was tried in April and GREW the bundle
~22 KB, because IIFE output needs inlineDynamicImports, which pulls every
chunk back in and adds a wrapper closure each. This is a plain fetch.
The base URL is captured as the script evaluates. Rollup shims
import.meta.url to document.currentScript, which is null once the script
has run, so a base read at mount would resolve against document.baseURI —
right on our own pages, wrong on every host that serves the bundle from
another path. deriveTreeBase() carries that same warning but returns the
tree root; these assets sit one level deeper, in the bundle's own
directory.
Each emitted file's sha384 is baked into the bundle and passed to
fetch({ integrity }), hashed from the same bytes the emit writes, so a
swapped catalogue fails closed to English instead of injecting text.
The fetch starts at module evaluation from the detected locale, so it
overlaps the bundle's own startup, and localeChromeReady() lets an RTL
locale hold its text chrome for up to 1.5s. For ar the flash is a layout
flip, not a text swap, which is worth a short hold; the map never waits,
and the timeout means a failed fetch degrades to English rather than to a
blank panel. loadLocale() runs after the instance exists, never inside
createI18n, because setLocaleMessage touches no effect scope while
createGlobal is the call that throws inside a stopped one. It also installs
into each instance that asks, since vue-i18n copies messages at
construction and a catalogue arriving later would otherwise reach only the
instances built after it.
Measured, doxa-simple-map: 284.6 KB gzip -> 165.2 KB, with ar as a 13 KB
asset. Ten assets emitted, every hash matching, ar no longer inlined, en
inlined.
The treeshake locale arm reads the narrowed build correctly (reports "10
asset") and fails when an asset is deleted. Its length floor is now per
script: a 25-character Chinese string is a whole sentence, so a
Latin-calibrated floor found no zh in a composite carrying short labels and
called it absent.
(cherry picked from commit 41d33d9a40)
A map says how it behaves on a narrow screen once, in its <profile>.meta.json:
"mobile": "desktop-only" (blocked on a phone) or "larger-screens" (opens,
under a dismissible notice). Absent means phone-ready. Until now the rule
lived only in the Research Gallery, as two hard-coded id sets, so a map
opened standalone on its own leaf page on a phone got neither the block nor
the notice.
- profile-meta.mjs knows the vocabulary; generate-tree and the dev manifest
carry the flag into each profile's manifest entry, and generate-tree into
the leaf page's BASE_CONFIG (with the map's name for the desktop-only
sentence).
- ProfileLoader mounts library/components/LargerScreenNotice.vue on a screen
under 640px: for desktop-only the profile is never mounted and a centred
message says why; for larger-screens a slim in-flow bar sits above the map
(it cannot cover the map's controls), dismissible per profile for the
session, in the element's own locale and direction. Inside the Gallery it
stays quiet: every Gallery pane now carries data-doxa-gallery.
- One text: the Gallery's two sentences moved into the library catalogue as
messages.largerScreenNotice / desktopOnlyNotice (+ noticeDismiss), all 11
locales moved as they were, and the Gallery reads them from there.
- The Gallery derives its rules from the manifest's flags. On a phone it
mounts nothing until the manifest has landed: its baked first tab (Big
Picture) would otherwise mount there before the flags say it is
desktop-only.
- dashboard-plus-grid is desktop-only and gospel-internet/board is
larger-screens, the two sets the Gallery held; nothing changes for either.
- check:mobile-gating now asserts the vocabulary, that each flagged leaf page
carries its flag, the Gallery's derivation and its closed-before-manifest
rule, that Big Picture stays desktop-only, the 11 locales, and the
notice's mounting. Its self-test plants one failure per rule.
generate-tree gathers assets in two stages: a pool, then an allowlist of
what the tree serves. ne-places.json and lib/poster-render.js were added to
the pool only, so every `bun run tree` deleted the served copies the two
commits before this one checked in, and a print would 404 the generator.
Both are now in the allowlist; a regenerated tree keeps them byte-identical
to their sources, and check:poster-render-pinned passes on it.
Code loaded at print time executes on partner pages, so it is pinned like every embed. The bundle
carries the generator's sha384 (library/packet/poster-render.integrity.js, written by
`bun run pin:poster-render`); posterSheets fetches <assets>/lib/poster-render.js as TEXT, hashes it
with WebCrypto, and on a match imports the verified text from a blob: URL — never the network URL a
second time. A mismatch refuses to run, with a message naming the URL and both hashes; the print falls
back to what it can do without the generator.
The page base is captured when posterSheets evaluates, and every URL it resolves (the generator, the
boundaries, the places) resolves against it — not the bundle's location, and not a URL a single-page
host changed after load.
check:poster-render-pinned (gates:pre), two-sided: the pin equals the sha384 of the source and of the
served copy (a stale pin names the command — proven by an unpinned edit); a TAMPERED copy is refused
and its planted side effect never runs; the genuine copy loads and draws. check:poster-browser-equiv
still passes byte-identical through the pinned path. country-posters 1,472,734 bytes (ceiling
1,500,000), my-upg-100-list 1,232,508, both carrying the pin; verify-bundles-shipped PASS; gates:pre 74/74.
development was RED on gates:post (verify bundles-shipped): country-posters.js had grown to 1.66 MB, over
the 1.5 MB ceiling, when the in-browser sheet generator landed. Measured by building without it: the
generator alone was 64.9 KB of the bundle (single-file builds inline any import), and Natural Earth's
places another ~170 KB (moved to a fetch in 8167415c5).
library/packet/poster-render.js is a pure module with no imports, so the tree now serves it verbatim
at assets/lib/poster-render.js (one generate-tree line) and posterSheets imports it BY URL at the first
print — resolved against the page, not the bundle, and kept out of the build with @vite-ignore. Every map
that can print a sheet sheds it; it is downloaded once, by a reader who prints, and cached across maps.
Measured on real builds: country-posters 1,662,962 -> 1,472,015 bytes (ceiling 1,500,000);
my-upg-100-list 1,231,789; verify-bundles-shipped passes; the bundle holds the runtime import and no
generator code. check:poster-browser-equiv now also asserts the served copy is byte-identical to its
source, and still passes byte-identical through makeZoneSheet and generateCountrySheet, which import the
served copy. gates:pre 73/73.
Generating map sheets in the browser inlined Natural Earth's capitals and cities (170 KB) into every
bundle that can print one: single-file builds inline lazy imports, so the whole page paid for a table
only a reader who prints needs. The tree now serves it beside the boundaries (boundaries/
ne-places.json, one line in generate-tree's copy list; public domain, no credit to carry) and
posterSheets fetches it at the first print, with the boundaries. No asset base, or a failed fetch,
draws no city dots — the generator's own rule for absent data. The Node generator reads the same file
from its source.
Measured: my-upg-100-list 482 -> 432 KB gzipped, country-posters 547 -> 496 KB. check:poster-browser-
equiv still passes byte-identical through makeZoneSheet and generateCountrySheet, which now fetch the
served table — so the fetch path draws the same dots Node does. gates:pre 73/73.
Ten agents read today's branch (five finders, one skeptic per area who had to confirm each finding
against the current code); 21 distinct defects survived. All are fixed here.
Generator and its proof
- generateCountrySheet fed only ADM0, so a country beside India or China printed without their
provinces (Nepal: 251 KB against Node's 921 KB). It loads the full boundary set, as the driver does.
- check:poster-browser-equiv rebuilt its own feed, so it could not see that. It now calls what the
browser calls (makeZoneSheet, generateCountrySheet — Nepal), deletes the target before the Node run,
runs Node with a clean environment, and FAILS (not "not measured") when the Node generator breaks.
Proven two-sided: it fails with the ADM1 fix reverted.
- The facts table's country list sorted in the reader's browser locale; pinned to en-US (Node's
default, so Node's output still hashes to the baseline).
- zone-sheet-print.js imports the library relatively, so bun, Node and the bundler resolve it alike.
Print seams and wiring
- The wall's Poster Map button used its own stored-file fetch and never the generated sheet, so every
country poster 404'd on the published tree; it now prints the generated sheet first.
- Country Posters wall: card stocks passed (its Card Deck could never print), iso3-names passed (zh/ar
page 1 kept English names), the stored fallback reads previews/ (where the tree serves them).
- Card-stock labels render at render time, so a locale change relabels them.
- The records provider caches by language AND list, and does not keep a failed membership load.
- The UPG-100 legend's Card Deck returns quietly on a parent row instead of an evaluator error; the
selection fetch uses PRINT_FIELDS.
Packet
- The centered-set caption counts what is DRAWN (a ring too full to separate reports its remainder).
- Each ring's number lane is sized to its own radius (ring 1 put a dot under its badge).
- Per-box line keeps a decimal below 10 people ("≈ 0" for small lists); worksheet rows name the country;
the localized poster kicker is fitted against the poster's real 19px, not 22.
Plurals and gates
- regions.countryCount / regionCount were authored in English zero|one|other order in de, es, fr, hi, it
and pt, so CLDR picked "27 Land", "1 países". Rewritten in each locale's own order; check:plural-forms
now fails a string in English order (proven on the old German string).
- check:optional-t-equivalence (gates:post) crashed on the new plural fallback's imports; it injects the
real cldrChoiceIndex and asserts the plural path (ru 21/3/5, en 0/1/7).
- check:packets-all and check-packet-render counted people as cards only, so every lightweight packet
read as "a packet of nobody"; worksheet rows count, and the browser gate checks the grid's top fill
(the renamed .lc-frame) instead of the retired scatter. check:packet-variants now reads the grid itself
— 5,200 boxes, Jesus' 1,716 from the top, the % shading from the top — proven: it fails on a renderer
that shades the last boxes.
gates:pre 68/68, check:optional-t-equivalence PASS, both bundles build.
Run on a real token against seven production leaves, the gate reported "no
sized canvas appeared within 30s" for all six of its targets, and its own
negative arm failed too. Three separate causes, all in the harness.
IT LOADED 404 PAGES. The gate asked for `${base}/${id}/` -- a bare directory
URL -- and internal/serve-strict.mjs answers 404 for those deliberately,
because the live layer does and a gate passing on a URL that 404s in production
is the divergence that server exists to prevent. Its header even records that
only ad-hoc probes and one gate ever used the bare-directory form; this was that
gate. Measured here against a real serve-strict: the directory URL gives 404
with and without the query, index.html gives 200 both ways. Both call sites now
ask for index.html.
IT JUDGED BEFORE THE TILES ARRIVED. waitForFunction resolved as soon as it
could return {engine, features}, and that object is truthy with features:0 --
so it stopped at the first sized canvas and read an empty frame. It now holds
out for a rendered feature and lets the timeout report a genuinely empty map.
IT COULD ONLY SEE TWO OF THE SEVEN MAPS. Features were read from
window.__rcmMap || window.__rdashMap, the two instances this repo happens to
publish, so the other five leaves read -1 and would have FAILED as "0 rendered
features" even having drawn correctly. An init script now wraps
maplibregl.Map/mapboxgl.Map as soon as the CDN script defines it and collects
every instance the page builds, whatever it names them.
The WebGL flags are NOT among the causes, though they were the first
diagnosis. A three-way A/B -- these flags, ANGLE's software backend, none at all
-- gets WebGL and WebGL2 in every case. They stay as they were.
The negative arm is not a bug either; it is why this was caught. It failed
because MapLibre genuinely did not draw, which correctly refused to certify six
"no sized canvas" results as broken production maps when the fault was this
gate's own URL. Its message now says to suspect the harness first when both
engines report nothing.
Still exits 75 here: no token on this box.