Collapse the legend, tap a pin, then close the detail: the legend comes back
open, though nothing asked it to. Reported against the leaf research map;
reproduced at 1440 in en, collapsed-first.
selectPeopleGroup saved the pre-detail legend state on every call that carried
a group, not only on the call that entered detail. The second call therefore
saved the 'open' the first call had just forced, on top of the 'collapsed' it
correctly saved on entry, and closing the detail restored that 'open'.
The measured part is why a second call happens at all. The pin click handler is
registered per map layer, and every visible pin has an invisible hitbox twin at
the same point -- same source, larger radius, added alongside it so taps are
easier to land. A layer-scoped click fires once per layer with a feature under
the point, so a single pin click calls selectPeopleGroup exactly twice, 3ms
apart. Measured on two different pins, both reporting both layers at the click
point; never more than twice. So ONE pick was already enough to lose the
collapsed state -- it never needed two, which is why this reproduced every time
rather than intermittently.
The fix is to make the method idempotent: save only when entering detail (no
group was selected before), restore only what was actually saved, then clear it
so a later detail cannot inherit a stale value. The duplicate binding is a real
defect in its own right and is tracked separately; this method should not have
depended on being called exactly once.
Verified two-sided against the shipped source, driving the real method body
rather than a copy: seven scenarios pass on the fix and four fail with the
previous body restored, while the scenarios that must be insensitive stay green
both ways. The poisoned run reproduces the browser measurement value for value
-- saved collapsed, overwritten to open by the second call, restored open.
Trace in _qa-cycle so the next reader can re-run it.
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.
A fresh reader given only the README and the HOWTO reached the Studio and a plugin's /__leaf page with
nothing else needed, but one step. After "port 8090 is already in use ... set DX_PB_PORT", neither
doc said the setting is read when bun run dev starts, so the new port only applies after a restart.
The README copies .env.example before starting the server, and the port message appears only once it
runs, so the steps looped with no stated way out. All three places that mention DX_PB_PORT now say to
pick a free port and restart.
The two scripts arrived without //scripts entries. The text is regions', written to match the neighbouring entries; its '8 self-tests' was re-read against the file this round carries (sha256 a886af2dca66e012, 124 lines).
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.
boardEngine.js was createWhiteboardMap under another name: the bodies are
identical line for line, and every import it made already came from library.
It is deleted and the four components import @map/maps/whiteboard-map.js.
The copies were NOT interchangeable, which is the part worth recording. Their
DEFAULTS are opposites, because the library file was extracted for the
whiteboard case and boardEngine served both map kinds:
boardEngine whiteboard:false boundaries:true centre [60,20] zoom 1.4
library whiteboard:true boundaries:false centre [0,0] zoom 1
Three of the four callers pass all four options explicitly, so they were never
at risk. GiMap — the geographic map — passed only a token and took every
default. Swapping its import alone would have set whiteboard:true, hiding every
basemap layer, dropping boundary attribution and moving the camera: a blank map
in production, from a change that reads like pure de-duplication.
So GiMap now states its four options. Every effective option is unchanged from
today's values, verified field by field, and the call reads the same against
either module.
Twenty-six references in capitals, which the earlier case-sensitive pass
could not see. Every requirement, date and measurement is kept: the 2026-09-23
instruction that removed the engagement and status filters, the MapLibre ask
with its Mapbox-compatibility condition, the blue-water-black-galaxy style,
the red ramp and the bin that collided with the heartbeat zone, the white
rings around the blue dots, the zoom that did not work inside the bounded map.
Two that are not quotes and were handled differently. The reduced-motion gate
opens with a medical reason — a reader of this map is photosensitive, and a
camera that sweeps the globe on every filter is a strobe — which is the whole
justification for the gate, so it stays as a fact about a reader rather than
about a person. And BpResearchHeader's "CAT-1 DRIVER CONTRACT" was a code
term; it is now the CAT-1 HEADER CONTRACT, checked for other references first.
Eleven lines broke and were found by re-reading every changed line, not by the
count. Removing an attribution left seven continuation lines holding the
closing half of a quotation that no longer opened, and four sentences carried
a pronoun for the speaker who had just been removed. The reference count was
correct at every point while the prose was wrong.
grep -wi driver across the three plugins: 0. The widened gate reports 40
swept, 14 still baselined. All six SFCs parse.
check:composite-freshness found stale on main: none. Rebuilt, tree + SRI
regenerated; the landing precondition (fresh, one folder, SRI) passed in main-capture scan.
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 check was defined and in no chain. So gospel.json's sheet.isoLine, identical in all 11
catalogues and referenced by no code, stayed on development through every landing since it
arrived; it surfaced only when someone ran the check by hand. This round deletes the key and
chains the check beside check:i18n-keys-exist, so it lands green and stays guarded.
Run before chaining, on this round's tip: PASS (18.8 s in the chain), 2 keys deferred pending a
ruling. On development without the deletion it fails on that one key, as gospel measured.
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.
A fresh reader given only the README and HOWTO-join-the-table got through install, bun run dev and
the Studio. One gap: no address for a single plugin's page. The Capability 1 fold says each plugin's
folder holds "an index.html for previewing it", which reads as /plugin-designs/<plugin>/index.html,
and that 404s under bun run dev. The page is /__leaf/<plugin>/<profile>, and the Studio's list is
/__plugin-gallery/manifest.json. Both are now named.
Three smaller notes from the same reader:
- the README copied .env.example to .env while the HOWTO uses .env.local. Both work and .env.local
wins; the README now uses .env.local too, which also keeps .env free for compose.yaml;
- the Mapbox token read as required; it is one of two ways to get a background;
- the 8090 port hint (DX_PB_PORT) was only in the HOWTO; it is now named where the README first
mentions PocketBase.
Also, the engine rule (was a follow-up commit, f0e965e4d):
Since 2026-09-28 useMapProvider.js resolves `auto` to MapLibre, and Mapbox is an explicit choice:
"provider": "mapbox" plus a token. The docs still said "Mapbox when a token is available":
- README's map-engines row;
- how-to.md's provider section;
- the plugin-designer skill's props table, which even says to confirm in useMapProvider.js.
All three now state the current rule. So do four lines that told readers to set provider maplibre to
avoid needing a key: README's Get it running and its "No token" fold, .env.example's token comment,
and reference.md's no-key row.
Case-insensitive git grep "Mapbox when a token" over README.md, documentation/ and skills/: 3 before, 0 after.
sheet.isoLine turned check:i18n-never-translated red: "· ISO 639-3" is identical
in all eleven locales, because a standard's designation correctly is.
The suggested fix was an ALLOWED entry, but the key has ZERO call sites — I
authored it during the label batch and never wired it. An exemption would have
made the gate quiet about a string nothing renders, which is worse than the red:
it spends the exemption list's credibility on a key that should not exist.
The line it was meant for (charts.vue:577) keeps "· ISO 639-3" as plain text, and
check:hardcoded-english does not flag it — correctly, since a standard's name is
not prose. So there was never anything to translate here.
Removed from all eleven. never-translated PASSES, parity and keys-exist unchanged.
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.
Chrome only, as scoped: the controls, status lines and navigation, not the data labels or the research notes. 192 English strings → 155; the 155 left are data and notes by that decision:
- the Semantic Trees reference (85: sample tree rows, API field names, taxonomy levels);
- the data-source panel entries (layer names, FeatureServer paths);
- the language family names and the tree level definitions;
- the index tiles' descriptions and source line;
- the cross-system ID names;
- five CSS/SVG values the scanner reads as prose.
What changed:
- The ten view names were written twice, English beside their tKey. The dead English goes, and the bar's brand is pplr.title.
- The theme and projection buttons on the three map views said "Theme: Dark (click to toggle)" / "Switch to Globe". They now say what they do, from the shared keys buttons.switchTo{Light,Dark}Mode and mapui.{enable,disable}Globe, as both title and aria-label.
- The sunburst's status is a key in state (pplr.status.d3Failed / readyD3), translated where it shows, so it follows a language change. Its count uses formatCount like the other views.
- The name-locales view's pill (pplr.pill.translations) and its data-sources button and header (pplr.ui.dataSources) are keys.
- The coverage scale's "0 locales" stop reads 0, like its other stops; the legend title names the unit.
- The index's title and help line are keys, and its tiles are named by their views' own names. Its old help line pointed at an Overview button that no longer exists.
22 English strings outside the catalogue. 10 were dead duplicates: the filter, tab and band labels were already translated from keyCities.* in the templates, so the English copies go. The rest now come from keys:
- the list's caveat is three translated lines, and its long sentence is ONE key rendered with useSentence, with the named-centre count and "Nominatim/OpenStreetMap" passed in as values (the name as its own left-to-right island, never translated);
- a row's group count reuses keyCities.list.peopleGroups, and the unknown internet pill reuses keyCities.map.unknown;
- a load failure reads "The data could not be loaded: {detail}" in the reader's language, with the server's own words as the detail (list and tables);
- the poster wall's intro and its "Other" region come from keys.
Headless, SSR with the real catalogue (_qa-cycle/exp-shepherd/check-keycities-i18n.mjs): the caveats render from keyCities in en, ar and zh, the gazetteer's name stays a <bdi dir="ltr"> island, no raw key or placeholder. The experimental English scan: key-cities 22 → 0. key-cities stays gated (dev only); it builds.
The starter every new plugin copies had 7 English strings outside the catalogue: profile A's "map engine not loaded" and all of profile B. Now profile A keeps its error's KEY in state (errorKey) and translates it in the template, with the engine's own message passed in as a value; profile B's heading, its note line and its sentence are keys, and "Served from {file} in the {bundle} bundle." is ONE key rendered with useSentence, the file and bundle names passed in as their own <code dir="ltr"> islands so each language orders the sentence itself. t() is called in the template throughout, never frozen into a constant at setup. The README's pieces table and "Your own text" section say the same three habits.
Headless, SSR with the real catalogue (_qa-cycle/exp-shepherd/check-template-i18n.mjs): profile B renders in en, ar and zh from template.json, with the sentence in each language's own order (zh puts the bundle first), both islands intact, and no raw key or placeholder. The experimental English scan: template-bundle 7 → 0.
check:no-new-driver-references matched "Driver" case-sensitively, so lines in
capitals passed unseen -- and my own greps were blind the same way, which is why
I reported this plugin clean twice. A case-insensitive sweep finds four, and one
of them carries a verbatim quote.
All four keep their engineering reason and lose the speaker:
· the reuse check states the negotiating position rather than whose it is;
· the substituted button font is blocked on a licensing decision, not a person;
· the code, not just the screenshot, is required to match;
· the table's ring and gridlines live in a global stylesheet, which is why
reading RegionDetail.vue alone could not show them -- the misspelt verbatim
quote is gone, the finding it pointed at is not.
Verified with grep -wi rather than the grep that missed them: 0 remain across
regions-dashboard and country-map. The four reasons are each still present by
their own text. sfc-parse, mjs-parse and no-new-driver-references all pass.
The gate matched "Driver" only, so "THE DRIVER'S ASK" and "HELD FOR THE DRIVER"
passed unseen — and so did my own verification, which used the same
case-sensitive pattern and reported zero. I checked the thing the gate checked
rather than the thing that mattered.
Seven lines across six files: five banner headings, one quoted requirement, and
the row-130 marker. Same rule as the first sweep — keep the reason, drop the
speaker — with the verbatim quote in build-doxa-gospel-access.mjs restated as
the question the map answers, since its content is the requirement and its
framing was attribution.
Comment-only: 0 non-comment lines changed, tree regenerates identically.
Case-insensitive grep across all three plugins now returns nothing.
check:no-new-driver-references matched 'Driver' only, so lines in capitals
passed unseen (and so did my own grep -w Driver). In my-upg-100-list:
'BATCH-UNION DRIVER' and 'gate drivers' are test handles, so they say so;
'in its driver' is generate.mjs's main routine; the zoning declarations'
'PENDING DRIVER' and 'In DRIVER-DECISIONS-DIGEST' become a pending product
decision and the product-decisions digest. grep -rni driver across both
plugins: 0.
Since the regional grouping, buildLegendNodes assembles the tree from the
LEAVES under the poster page's nine region sections. The old India, China,
"Asia (admin-1)" and "Frontier Asia (uncovered)" parents were still built
and never returned: dead code whose English labels the call-site triage
read as reader text. They are deleted, with sumCount, which only they used.
The legend's output is byte-identical for all four configurations
(with/without records, with/without the localizing options; 2,107 records).
The L15 shape assertion now composes its text inside the throw: the
offending leaves are collected as data and formatted in new Error(...), so
the message is visibly developer text. Same message, checked on bad input.
pop-proof.mjs had been failing on every run, because section 3 looked up
those same dead parents, and section 1 expected their count (32). It now
rolls up the nine region parents the tree actually has (each pop = the sum
of its children, and there must be at least one) and expects 28 leaves + 9
regions = 37. ALL PASS, identically against the previous legend-tree.js.
Fourteen references, each rewritten to state the requirement directly. Every
date, measurement and pixel coordinate is kept — the y=1827/y=1865 redundancy
that prompted the count line, the 2026-09-22 interoperability report behind
the Language and Status props, the 220x130 letterbox the square image
replaced, the dev-server path that showed "view more information" reaching
the wrong page, and the India click that left the grid unchanged.
Thirteen were found by a case-sensitive search and a fourteenth was not: a
line in capitals, carrying a verbatim quote, which `grep -w Driver` cannot
see. The check that catches both is `grep -wi driver`, and it now reports 0.
Two sentences broke in the first pass and were found by re-reading every
changed line rather than by the reference count: one left "the control the"
with its object removed, and one left a quotation with an unclosed quote when
the surrounding attribution went. The count was correct while the prose was
wrong, which is why the count is not the check.
check:no-new-driver-references passes. research-map-plus-grid and
dashboard-plus-grid both rebuild byte-identical.
rega at 390 (b07fc59e0, en + ar, both views): the sheet lowers and raises as it should, but
- the map's attribution control sat under the sheet, lowered or raised, in all four cases. On a phone the map's bottom control corners now sit above the sheet: 55% + 6 px when it is up, 62 px when it is down;
- a pick with the list lowered moved the map but showed no detail, because the detail is in the sheet. A pick (a group, a neighbour dot, a bridge language, a group opened from Campaigns) now raises the sheet FIRST, so the frame is computed for the sheet that shows the detail;
- the hover tip was cut off at the right edge in ar ("Ko…"): it opened to the right of the point wherever the point was, and after a tap it stayed where the tap had been while the map flew to the pick. It now opens toward the side with more room, is at most 45% of the map wide, and clears when the map moves.
check:composite-freshness found stale on main: template-bundle. Rebuilt, tree + SRI
regenerated; the landing precondition (fresh, one folder, SRI) passed in main-capture scan.
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.
rega's poster-path timings: A4 300 dpi ~42 s, 24x36 in at 150 dpi 90-135 s,
24x36 in at 300 dpi ~4 min. Through all of it PosterDialog showed an
internal phase name ("hidden-map"), a bar measuring only the current phase
(so it restarted and sat still), and English. A failed render, including a
canvas too large for the device, went only to the console: the status block
vanished and the reader was told nothing.
- The bar follows the composable's overall, monotonic percent; beside it,
the time elapsed (ticking) and, while stitching, "tile n of total".
- An honest expectation for the chosen size, from its megapixels: "up to a
minute" from 5 MP (A4 300 dpi, 8.7 MP), "a few minutes" from 15 MP (24x36
in 150 dpi, 19.4 MP, and up). A4 at 150 dpi (2.2 MP, 11 s) shows none.
- A failure is shown in the dialog: a specific message when the device ran
out of memory (an oversized canvas or encode), a general one otherwise,
nothing when the reader cancelled (AbortError).
- The Render button's labels and the "(optional)" placeholder go through
T() too. 9 poster.* keys in 11 locales, using the dialog's own terms.
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.
Defence in depth for a bug already fixed at its source. <doxa-country-map> was
passing center: null when its host set none; Vue applies a prop default only for
undefined, so that null replaced this component's [20, 10] default and reached
the Map constructor, where the engine's LngLat.convert threw before a style or a
tile was requested. Twelve measured cases, every one a dead map reported as "a
map-token or connection problem".
The element now sends undefined and no longer produces that null. These guards
are for the NEXT caller: a component in a library cannot assume every host
resolves an absent value the way one of them does.
The asymmetry is why it happened at all. `bounds` two lines below already had
`|| undefined` at the constructor, so a null bounds was harmless; `center` had
no guard, so a null centre was fatal. Both now behave the same way.
flyTo gets the same treatment, because it reaches the engine's LngLat conversion
by another path and would throw identically on a re-frame. It falls back to the
map's current centre, so a caller that supplied none gets the zoom re-framed
without the camera sliding sideways.
`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)
gospel-resource-rows.js builds each resource row's display text, and its
fallbacks were English literals: a record with no name of its own showed
"Scripture in this language", "Audio Scripture", "Listen in a browser" to an
Arabic or Chinese reader — precisely the rows whose source data was least
complete. check:hardcoded-english does not read .js files, which is why the
bundle's sweep never saw them.
Thirteen keys in eleven locales, and the translator is PASSED IN rather than
imported: the module is a pure function with no component context, and it stays
optional, so a caller without one degrades to the English written at the call
site instead of throwing.
Three publisher names stay verbatim — JESUS Film, Scripture Earth, Zume — but
where a name sits inside an ordinary phrase the phrase is keyed and the name is
INTERPOLATED, so "Zume discipleship training" translates around a brand a
translator never retypes. SE_KIND's values are displayed and take keys; its keys
are the upstream feed's codes and must not be touched.
Verified by running the module both ways: without a translator every row reads as
it does today; with an Arabic one the labels and scopes are Arabic while
"Scripture Earth" and "Zume" are unchanged.
ROUTED TO STUDIO, not self-landed: the eleven locale files make this a library
change. The route test says 11, and the last time I judged a landing by which
plugin the work was for rather than which directories it touched, the scan ran
against most of the fleet and timed out twice.
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.
country-map carries the undefined-centre fix; regions-dashboard carries the
localised card names and India's translated reason. tree then fix:sri, 127
agreed and 0 stale.
<doxa-country-map> sent center: null when the host set none, and null is not
absent. Vue applies a prop default only for undefined, so an explicit null
REPLACED RegionCountryMap's [20, 10] default rather than falling back to it. The
null reached the Map constructor, and the engine's LngLat.convert threw before a
style or a tile was ever requested.
So any host that gave countryCodes without a centre got a dead map -- and the
page told them it was "a map-token or connection problem", which blamed the
wrong thing entirely and sent two rounds of investigation after providers and
tokens. Both center and bounds are documented OPTIONAL in this element's
contract, and the manifest's own embed snippet sends neither, so a partner
following our published snippet got nothing.
MEASURED on a host page across twelve cases: MapLibre selected correctly in all
of them, 0 requests to mapbox.com, 0 responses >= 400, 0 failed, 0 unanswered --
and no request to the tile host at all, because the map never got as far as
asking. The console carried the real cause the whole time.
The element now sends undefined, which is what "the host set none" means. That
alone restores the default and starts the map; the defensive guards inside
RegionCountryMap go separately as a library change, because they protect a
different caller rather than this one.
Probe reads the shipping source and states Vue's own rule independently, since
that rule is what makes this a bug. It reports whether the library guards are
present rather than requiring them, so it stays honest on either tree.
Two untranslated strings on a production print surface, both reported by the
Country Posters plugin after it fixed the same pair on its own wall.
THE CARD NAMES. postersFor returned the country-posters.json record unchanged,
and that record carries its label in English only -- so every card with a poster
was named in English in every locale. label is not decoration: it is the card's
accessible name, the print HUD's title, and the title on the printed list, deck
and packet. It now goes through countryName(iso3, locale) for a non-English
reader. English keeps the data's own names, because Intl's English differs from
them for 32 of these 175 countries and that swap is a recorded product decision.
The sheet generator still receives the original record; the poster itself is
English by the same decision. The locale REF is read, not a snapshot, so a
runtime language switch renames the cards.
INDIA'S REASON. posterUnsupported returned the record's own English note, so one
untranslated sentence sat on the wall in every locale. The record still decides
WHETHER a country is unsupported; only the wording now comes from the catalogue.
The key needs no number in it because the card shows the count directly above.
★ AND IT NEEDED A SECOND TRANSLATOR, which is the part that would have shipped
broken. This page's t() is useRegionsI18n's: it resolves DX-life's English
sentences as keys inside the `regions` namespace, because that is what the page
is cloned from. messages.countryPosterTooMany lives in another area, so t()
would have missed and rendered the literal string "messages.countryPosterTooMany"
on the wall. tKey is vue-i18n's own lookup, added for keys outside this page's
namespace.
Verified in the built bundle rather than the source: DisplayNames present,
countryPosterTooMany present. A probe over the real alpha-2 table confirms the
localisation across ar/ru/zh/de/fr and that an unknown iso3 falls back instead
of throwing. gates:pre 75 PASS / 0 FAIL / 1 NOT MEASURED of 76; i18n-parity and
i18n-keys-exist both pass. Cost +244 B.
THIRD-PARTY.md's two open licensing items "need an owner's decision" instead of going to a named person.
needs-eyes describes the browser policy and a maintainer's laptop as the second pair of eyes. The
skill and its glossary say the Experimental section was replaced by capability tiers, with the same
pointers into plugin-studio.vue. grep -w Driver over documentation/ and skills/ is now 0;
check:no-new-driver-references passes.
FIX-RUDDER-PANEL-DRAFT-LAST-LINE-UNREACHABLE-WITH-TALL-HEADER.
.compact .compose carried min-height:120px, so the column refused to shrink and
whatever the header took was pushed past the panel's clip. The draft's own
scrollbar cannot help: the overflow is OUTSIDE the scrollable area, which is why
a wheel over it reaches the end of its content while the last line stays hidden.
Measured in the corner widget with the engine offline, header 237px:
390x560 — panel leaves 99px, the floor forces 120, overflow 21px.
375x450 — panel leaves 33px, overflow 87px.
Both reproduced independently, and both are exactly 120 minus the room available,
which is what makes the floor the mechanism rather than a coincidence.
Pre-existing: present on plain development as well as on my earlier branch, so
this is its own defect and not something a previous round introduced.
The floor is removed unconditionally. A viewport-height guard would not work —
what consumes the room is the HEADER, and a notice can make it 237px at any
viewport; an earlier version of this fix guarded above 520px and would have left
the 560px case still broken.
Cut clean onto development rather than carried on the branch it was written on:
that branch also held two hunks that were deliberately withdrawn after an A/B
showed they changed nothing measurable, and landing it would have quietly
reinstated them.
check:composite-freshness found stale on main: doxa-maps-spa
plugin-studio
prayer-walk-planner
template-bundle. Rebuilt, tree + SRI
regenerated; the landing precondition (fresh, one folder, SRI) passed in main-capture scan.