Comments in library/ and internal/ that quoted a person, attributed a decision to one, used first person,
named fleet agents or carried private host names now state the same technical fact in neutral words:
poster-render (including the comments it writes into generated posters), packet-render, the poster and
research scripts, PosterWall, DataTable, the legends, the layouts and the gates. The network-policy note
states the policy itself. Behaviour is unchanged: a few printed gate messages and reason labels drop an
agent name, the poster-render integrity pin follows its comment-only edit, and series.json changes only in
its _meta and _why notes. The historical poster design snapshots (library/scripts/posters/snapshots/),
which nothing reads, are removed.
staging-reference-v2.html's header states its design in plain words instead of a dated quote
(nothing reads the file; only file-map.md names it). THIRD-PARTY.md and
reference-universal-props.md no longer name an agent.
Comments, messages and docs only; no check logic changes.
- The role-word gate is renamed check-no-internal-role-nouns (and its baseline fixture). Same
logic: it builds the word from parts so the file does not carry it, and a planted line in
title case and in capitals still fails it while WebDriver and "GPU driver" do not.
- public-hygiene-check: the two role patterns are built from parts (the regexes are byte-
identical), the header loses the forge and date wording, and the prayer comment loses its
decision date. Its output on the tree is identical in normal and --strict modes.
- landing-preconditions, check-no-public-binds, check-published-tree-weight: no fleet tool
name, no "this box", no forge name in comments and messages.
- Removed: check-backlog-refs, check-backlog-status-enum, check-done-evidence,
check-verification-tags and check-dxlife-untouched (they police a private backlog and
workspace), with their scripts, docs, gate-list entries and orphan-baseline entry, and the
committed needs-eyes pages (check-embedded-rtl --emit writes them on demand).
- package.json docs: no agent names. Docs: the stale check mentions and the snapshot
references are gone (the snapshots themselves leave in the studio P commit).
gates:pre: PASS 115, FAIL 0, NOT MEASURED 1 (browser seat).
series.json: a new `indiaState` series, modelled on `country`, so a state poster names itself honestly
("India State Maps", "What is a State Prayer Map?", one sheet for each state and union territory of India,
"which state God has put on their heart"). The country series' text would have called Gujarāt one of the
~170 Country Maps. Approved as the default wording; shown as editable.
check:series-isolation, two blind spots found by adding a third series:
- Shared wording was dropped from only the EARLIER series (sets were mutated while still being compared),
so the later one kept it as a fingerprint and the earlier failed for carrying it. The shared set is now
found on the original sets. Self-test: three series in any order; the old loop fails it.
- The contamination scan never read a series' `poster` block, where a country poster once printed the
UPG-100 side box. It does now. The phrases that NAME country and indiaState are hand-listed, so a
verbatim copy of one's framing into the other is caught even though symmetric dropping treats it as shared.
Self-test 9/9, including both directions of a country/state copy.
countrySheet.js: the seriesFor comment no longer says "map 1 of 0" was printed; it was latent (the map-only
packet page prints no series line, and the India poster is gated), as a rendered check showed.
Its two panes, Styles and Sizes, are independent views of the poster
templates; neither drives the other, so it gets a NO_LINKS entry with
that reason rather than invented links.
The TOK-LITERAL self-test planted a string shaped like a real Mapbox token,
which is itself what the rule forbids and reads as a leaked credential to any
secret scan. The sample is now built at runtime from plainly fake parts
('example', 'not-a-token') in the same pk. + base64url + tail shape, so the
self-test still proves the rule fires, and no token-shaped literal is left in
the tree.
blessPoster.js now plans its 5 x 8 ft sheets, finished size, screen layout, glue band, trim line and sheet
tags through library panelSheets.js instead of its own copy. The print HTML, the screen layout and the plan
are unchanged: hashed for 3 presets and the default in all 11 locales (44 cases), before and after, all
identical; a 0.001 in change to the glue label moves all 44.
check:panel-sheets (gates:pre, headless) pins both geometries (60x96 stacked, 66x66 side by side, one
sheet; glue and bleed):
- the sheets tile the finished poster;
- glue slices tile it, and bleed slices repeat exactly the join;
- a bleed join prints a guide, not a solid band, and a screen preview always shows the red band;
- the Bless 5x8ft takes its bands and marks from the module.
It is two-sided: 4 planted defects are caught, and the Bless arm fails on the old private copy.
check:poster-wall-host-seams gains arm H: a default wall's broken stored
webp retries the tree's own copy before the placeholder. Arm I: a generated
preview that fails shows the placeholder at once. The gate serves embedBase.js
as if from a built tree, because the second base is derived from that URL and
is empty from source, which let a mutant that skipped the retry pass.
previewSrc's observer now uses the wall's own scroller as its root (the
viewport when the wall is embedded and the page scrolls it). A pane wall
scrolls inside .wall, which clips its cards, so with the viewport as root the
200px margin never looked ahead. Arm E asserts the root.
PosterWall states the two limits of previewSrc: a timeout ends the wait, not
the render, and the page cache never revokes the URLs it holds.
A card's thumbnail was always the stored previews/<id>.webp. A gallery of
poster templates drawn from the library has no stored file, and must show a
library change the moment it lands, so the thumbnail can now come from the
host: previewSrc: async (item) => an image URL | null.
Default null keeps the stored webp and creates no observer, so every current
wall is unchanged. When set, a card renders only once it is in view, one
render at a time, cached on the page by instanceId + item id. Until it answers
the card shows its label; null, a throw or no answer within 20 s shows the
existing "preview unavailable" placeholder.
The header-action gate becomes check:poster-wall-host-seams and covers both
seams: arms D to G for previewSrc (default webp and no observer; lazy, one at
a time and the null fallback; the timeout; the cache). Six mutants of the new
code each fail it.
The home link joined Collapse's rule (.vtl-collapse, .vtl-home { … }) so the
two share one box, and the gate only recognised a rule that named
.vtl-collapse alone, so it found no reference and failed on development. It
now reads the reference from any rule whose selector list names
.vtl-collapse as a whole selector (not :hover, -tx or a descendant), and its
self-test gains a poison that moves only that rule's radius.
A template card (Poster Design) carries no people-group count, and the new
branch printed a bare ' UPG ·' before its label. The count and separator now
render only when item.count is set, in the headerAction branch only; the
default toggle is unchanged. check:poster-wall-header-action gains arm C for
it, which fails with the guard removed.
esc() prefixed a single quote to a value starting with = + - @, but OWASP's
formula triggers also include a leading TAB and CR, which a spreadsheet can
strip before reading the = behind them. Every map's CSV export shares this
escaper. The shape gate carried a formula probe but never checked it came
back neutralised; it now does, for =, TAB and CR, by slug.
The sticker on every poster card was hard-wired to "Show on map". A wall with
no map beside it (a gallery of poster templates) needs the same card with a
different action, and a fork of the card would drift from the wall.
headerAction null (the default) renders the existing toggle unchanged: the
same markup, aria-pressed, the is-last highlight and the window event, so
every current wall is untouched. { labelKey, ariaKey } renders a plain button
labelled from those keys that emits 'header-action' with the item and
dispatches no window event.
check:poster-wall-header-action (gates:pre) mounts the real PosterWall
headlessly on a recording renderer and calls the sticker's onClick in both
modes. Four mutants fail it: no seam, a new branch that also dispatches, a
default that emits, and aria-pressed on the new branch.
One more people group moved from Unengaged to Engaged upstream; the total is
still 2107. The maps compute from the live rows and already show the new
split (read on Prayer Progress's engagement legend), no shipping code
hardcodes the totals, and the live endpoint was read independently, so only
this baseline lagged.
documentation/how-to-build-mobile-apps-from-plugins/README.md explains why Deep Research
(doxa-maps-spa, its app preset) is the plugin to wrap first: the seven production maps in one
phone-ready shell, token-free on MapLibre. It then walks the whole path: tools, the wiring check,
one command, what each step does, another plugin as the app, making it your own, and fixes.
The one command needed two small changes; the default app is unchanged:
- internal/set-app-start-page.mjs takes APP_START=<plugin>/<screen>[/<preset>] and opens that page
instead of the UPG-100 list. Unset, it behaves and marks the page exactly as before. A malformed
value, a page missing from the build, or links that would not resolve are refused, as before.
- package.json: phone:from-tree is sync, start page, verify, assemble, install from the committed
tree, with no full rebuild (bun run build refuses without BUILD_ALLOW=1).
phone:deep-research is phone:from-tree with Deep Research as the start page.
how-to.md links the guide, and two of its lines are corrected: webDir is public/dx-maps/, and
apk:debug's first step is the full build, which asks for BUILD_ALLOW=1.
Checked, with no Android tools: the start page written for doxa-maps-spa/doxa-maps-spa/app, then
the app root served and loaded in the shared browser at 390 and 1280. It showed the gallery
element, 15 tabs (the seven maps and the experimental ones) and a drawn map, with 0 console errors
and 0 failed requests. --check passed for APP_START and failed for the default until it was
restored; the default run still writes the UPG-100 page with its original marker. The Gradle and
adb steps are those bun run phone already uses; they were not run here.
The host page of every full-page map (and the Plugin Studio) sized the
element to the viewport in a way that tracks the phone's address bar:
- the built fixed-viewport leaf used position:fixed with inset:0, which
follows the DYNAMIC viewport, so the map and its legend sheet resized
on every scroll that moved the bar;
- the Plugin Studio page and the dev server's leaf and studio pages used
100vh, the LARGE viewport, taller than the view while the bar shows, so
the page itself scrolled by the bar's height and the app seemed to
resize.
All four now use height:100svh (100% first as the fallback where svh is
unknown), anchored top-left, and set --profile-min-h:0 so ProfileLoader's
100dvh floor cannot grow the element when the bar hides. On a desktop svh
equals the viewport, so nothing moves there. Scroll-page leaves (the
cap-4 stacks) are unchanged.
Regenerated the tree: 51 leaf pages and the Plugin Studio page change
only this rule and its comment; no bundle changes.
Point 4 (READ THE WORD) printed Mark 5:1-20 in full at 14px, the longest
and smallest text in the packet. It now prints THE HEAVENLY FATHER'S
HEART: one heading, one framing line and seven short readings (Noah,
Lot, Rahab, the Gerasene, Cornelius, Lydia, the jailer), each a
household the Father saved through one person. The names come from each
locale's block; the addresses stay canonical English in the data and the
book is named in the packet's language, like every other reference
(Genesis and Joshua added to the 11 books tables).
That room goes to the twelve points: body 17 -> 22px, heading 19 -> 24px,
references 14 -> 17px, and 13 -> 16px between points. The page still fits
one sheet in all 11 locales (the tightest, de, ends 59px above the
bottom).
The gates that asserted the full passage (packet-render, packet-browser,
poster-prints) now assert the list: a .p4-heart block with its seven
readings. Rebuilt: my-upg-100-list, country-posters, regions-dashboard,
doxa-research-map, upg-neighbor; the tree and SRI regenerated.
At two posters per row on a phone (a ~162px card) the print row wraps to
three lines, and because it is a sticker pinned over the banner's bottom
edge that grows downward, it hung about 45px below the card's white box.
Each card is now fitted to its own row: a ResizeObserver on every .pfs
writes --pfs-room on its card when the row hangs past the white box (the
part below the banner, plus 8px of daylight, that the 26px bottom padding
does not cover). A row that fits writes 0, so every card that already
contained its row is unchanged.
check:poster-card-fits asserts it: a print row may not end below its own
card (below-card N). Rebuilt: my-upg-100-list, country-posters,
regions-dashboard; the tree and SRI regenerated.
The gate began from keys like 'adm0:IND', so it never ran resolveTerritory
on what the map hands the click handler, and it passed while every tapped
country resolved as a province. Section 4 feeds the resolver three real
tapestry shapes: a country (shapeName + iso3, no listId), an India state
(listId + shapeName + iso3), and a country drawn inside an Asia zone
(zone.adm0).
Two-sided: on the previous resolver it fails three ways (Sudan and Nepal
filed adm1 with null counts); on the fixed one it passes.
The bold header on a poster -- the UPG count and population set large, a
bigger map, and no "Jesus' Strategy to Feed the 5000" / "What is a Prayer List"
columns -- is the SPARSE layout, the "thumbnail-worthy" design. The `posters`
script generates with SPARSE=1, so every stored thumbnail has it. But the env
handed to the generator in the browser carried only FORMAT and POSTER_DATE, so
EVERY sheet drawn at the click -- UPG-100 and country alike -- fell back to the
dense layout.
MEASURED on FINAL (two UPG-100 zones, Northwest and South-Central India): the
thumbnail shows the SPARSE header; the print the reader actually gets shows the
small facts table, both vision columns and a smaller map. That is the reported
"the country poster looks worse than the UPG-100 poster" -- the comparison was a
SPARSE thumbnail against a dense sheet.
(a) posterSheets.js passes SPARSE for every POSTER format and NOT for 'packet'.
That is exactly the split the two build scripts make: `posters` sets
SPARSE=1, `packets` does not. The renderer's formats are 3x4, 3x3, letter and
packet, so 'packet' alone keeps the full layout its pages were built around.
(b) check:poster-browser-equiv's poster case now runs the Node side with
SPARSE=1 as well. It used to compare dense with dense, so it passed while no
reader ever saw that layout on a card. The packet case stays dense on both
sides. PASS: country-npl 909026 bytes on each; asia-india-gujarat (packet)
835047 on each.
The stored UPG-100 thumbnails are untouched by this. What changes is the PRINT,
which now matches them.
Found in the max-effort re-read of 1ab8524cf. countryZone now colours the sheet
from the region, and this gate's zone omitted it -- so it rendered the FALLBACK
teal and logged '[posterSheets] region undefined is not in POSTER_REGIONS' on
every run. The gate still passed, because what it pins is the served generator's
integrity rather than the colour, but it was exercising the path that does not
ship and shouting about it.
Chad is Africa in library/data/country-posters.json. The warn-once is doing
exactly its job here: it found a caller that had not been updated.
From studio's review of 87f10ceda. configDest swallowed a JSON parse error, so with a malformed
vite.doxa-maps-build-config.json (one trailing comma from a hand edit), --if-present read it as "no
destination configured" and exited 0. The build went green and never refreshed the site. The parse
error is now recorded when the file EXISTS. Under --if-present or --from-config the command exits 1
with "build config unreadable: <file>: <error>", even with DOXA_DROPIN=off. A config file that is
absent, or one with no dropin.dest, still skips.
Tested 7 ways:
1. dest configured, host absent: skip, exit 0.
2. host present: 1,208 files copied, exit 0.
3. no dest key: skip, exit 0.
4. no dest key, plain --from-config: exit 1.
5. trailing comma: exit 1, "build config unreadable".
6. trailing comma with DOXA_DROPIN=off: exit 1.
7. no config file: skip, exit 0.
The config was restored byte for byte (cmp).