A device whose fonts lack U+0671 printed it as a box. Only those 13 code points change (0671: 13 -> 0, 0627:
+13); the bold verse is still a substring of the passage. check:packet-strings-parity fails on any U+0671 in
an ar value (it fails on the previous file). The Arabic scripture blocks also name Arabic faces before serif.
generateSheet gains `sparse` (default: every format but 'packet', so every existing caller is unchanged);
the UPG-100 zone sheet answers the packet's 'packet' request with the full letter poster, the same
1500x1941 page. check:poster-browser-equiv compares that sheet with Node's letter format without SPARSE.
The wheel took the source's hex palette, rim badges with a minutes tag and a Start-here mark; the gate still
counted fill^=hsl paths (0) and every non-numeric text (25), so it failed every packet with a correct wheel,
the pilot's included. Two-sided: PASS on the lightweight packet in en and ar; FAIL on a copy with the
segment labels blanked (0 labels) and on one with the segments unfilled (0 segments).
The print-time files are pinned (poster-render.js and its classic copy,
packet-strings.json, ne-places.json, india-states.json) and were fetched with
cache: 'force-cache', which returns any cached copy, however old. After a
re-pin, a reader who had printed before kept the previous copy: it failed its
pin on every print and the print fell back without a word. Page 1 became the
stored poster, a non-English packet would print in English, and India's state
cards would be lost.
sha384.js fetchPinned fetches force-cache first (fast, and offline once
cached) and, when the copy is not served or fails its pin, once more with
cache: 'reload', which skips and refreshes the HTTP cache; only that answer
decides. posterSheets (the generator and the places table), packetStrings and
indiaStates all fetch through it. The classic-script path refreshes the cache
the same way and retries a refused tag once.
check:pinned-fetch-fresh models the HTTP cache with stale copies: page 1 still
draws, the packet keeps every language, India keeps its 36 states; a matching
cached copy is used with one request; wrong bytes are refused. The previous
loaders fail it. Self-test poisons: no fresh refetch, never the cache, a
mismatch accepted, a loader fetching its pinned file directly.
What the three columns ask; the blessing to pray over each group, Numbers 6:24-26 in full with its credit
(the public-domain text per language, compared verse by verse with its source; ro and hi print the English,
as the packet's other scripture does); one line of further blessing references (the Synodal's own Psalm
numbers in Russian); the prayer wheel at the end; and a pace table, daily (strongest) to every three months
(slower), weekly marked as the healthiest, each pace's daily load computed from the list's size.
The first page holds 11 rows (16 before); later pages keep 18. Measured with a 100-group list in all 11
languages: the longest first pages (ru, hi) keep 77px under their last row, the references fit one line.
check:packet-variants asserts the description, once, before the first row, and the computed pace; the
render gate follows the new pagination.
The tick boxes become tally cells, on the lightweight worksheet and on the full packet's profile rows, so a
list prayed through again and again counts every time. The headings are the questions themselves, broken
into balanced lines; the worksheet's tally columns widen to 160px so English heads take two lines. The
worksheet's note no longer says there is no pace to keep. Measured in all 11 languages: every worksheet
sheet and every profile row still fits (16 + 18 rows a sheet unchanged).
A second read measured a hole in the phone print keep: a touch after print()
started the grace, and if the print UI then opened (the page hidden, or the
window blurred), nothing cancelled it, so the document could go while the
reader was still choosing Save as PDF. Now:
- going away again during the grace cancels the pending drop, and the next
"back" counts;
- BACK_GRACE_MS is 60 s (was 5 s): a slow phone can still be writing an
11-page packet after its dialog closes, and the kept document is invisible
on screen and replaced by the next print;
- a touch in the first TOUCH_SETTLE_MS (2 s) after print() is not "back" (a
double tap while the preview builds).
check:phone-print-keep gains arm H (a touch, then the print UI, by hiding and
by blur: still mounted past the grace, released after the next back) and arm I
(a touch in the settle window), with the poisons "no cancel when away again"
and "no touch settle window"; arm C now taps Print a few seconds after the
document is ready and stays in the dialog past the grace. The first cut fails
H and I; the pilot's file fails A to H. Desktop stays byte-identical.
On a phone, printDocument mounts the document into the page (#dxm-print-root and
a print-only style) and calls window.print(). It dropped both at afterprint. But
Android Chrome's print() does not block: afterprint fires once the preview is
built, and the file "Save as PDF" writes is rendered later, after the reader
taps Save, from the page as it is then. The preview showed a packet's 11 pages
and the saved PDF was the bare host page.
The document now stays until the reader is back on the page: shown again after
being hidden, focused again after losing focus, or touched after print(), then
BACK_GRACE_MS more for a job still rendering. A new print replaces it, a
cancelled one drops it, and the ten-minute ceiling stays. Listening starts at
print(), so the Print tap of a late print does not count. printImageBlob's
object URL now follows the document (onRelease) instead of afterprint. On
screen the document is display:none, as before. Desktop prints through the
frame exactly as before.
check:phone-print-keep runs the phone timeline on fake timers and a stub DOM
with an Android user agent; the previous file fails it (gone after afterprint,
image revoked). Its self-test poisons the old drop at afterprint, no release
when back, listeners armed at mount, no grace, printImageBlob revoking at
afterprint, and no ceiling.
Styles and Sizes now name every size the one way (src/sizeText.js, shared):
templates.js carries each size as data (inches, side lanes, panels, paper),
and posterDesign.sizes gains sideLanes, packetPage1, panels and anySize in
all 11 locales, so Arabic readers no longer see "5 x 8 ft, side lanes" or
"Letter, portrait". A plugin's declared size (a ladder key, letter, a4) is
named through the shared ladder.
A plugin's design title came from its labelKey, but Poster Design's bundle
ships only the catalogue areas its own code names, so a plugin's own area
(upgMap, ...) read English. internal/profile-meta.mjs now resolves each
block's labelKey in every locale at build time and carries the titles in
manifest.json (posterDesignLabels, only beside a posterDesign block), and
Styles reads the reader's language from there.
The measure-once design missed a host restyled from sized to height:auto after mount WITH in-flow content: it
stayed sized at its content height instead of getting the floor back, as the old probe did.
- After mount, follow() re-probes only when the reading says the host may have changed kind: floor on and the
host shorter than the floor (styled after mount, or capped), or floor off and the host changed size (it may now
follow its content). The re-probe saves every scrolled descendant (through shadow roots) and puts it back
before the frame paints, so no reader state is lost. A window-height change on a content-sized host still
writes nothing.
- A host capped below the floor (max-height) is latched by the probe that finds it, until it reaches the floor
again, so it is never re-probed in a loop. The 1 s post-flip window is gone.
- check:host-floor gains arm H (sized → auto with 420 px of content: the floor returns, at most one flip, the
scrolled child keeps its scrollTop) and counts re-probes in D/E. Poisons: the old per-resize probe, no capped
latch, no re-check of a sized host, no scroll restore.
- library/elements/hostFloor.js holds the decision ProfileLoader uses: probeOnce() at mount, follow() on every
host resize (reads only; at most one flip of the floor).
- A host CAPPED below the floor (max-height) follows its content up to the cap. It is found at mount, or, if
the cap arrives later, by the host shrinking right after a flip; either way it is latched as content-sized
and never flipped again, so it cannot loop. The post-flip check runs before the collapse rule, so a capped
host with empty content is latched rather than flipped back and forth.
- check:host-floor (gates:pre; self-test in gates:post): a stub DOM with a scrolled child that clamps when the
loader collapses. After mount, host resizes never write the loader nor move the child; a sized host fills;
late sizing flips once; a max-height cap (at mount: 0 flips; after mount: at most one flip and one revert)
never loops; a collapsed host gets the floor back; an inline host is left alone. Poisons: the old per-resize
probe, and a missing latch.
Tightened before the India tab lands. A built bundle that READS globalThis.DXM_PREVIEW_UNRELEASED would ship the
withheld path itself, behind a global that any embedding page or console can set before the element loads, so
withheld content would print from production.
- A: no file under public/ names the flag at all (was: no assignment).
- B: every source line that reads OR writes it carries import.meta.env.DEV (was: writes only), so each read
compiles away in a build.
- Self-test: a built setter, a built read, an unguarded source setter and an unguarded source read must each
fail; the DEV-guarded forms and comments pass.
A pin's position is generalised, so it does not tell a reader the locality or where a group is
centred.
- pins.sharedPoint in all 11 locales and ApproxPinNote.vue's fallback: the groups sharing the point
are named and counted ("{n} groups share this dot"); the locality claim is gone.
- keyCities map.note, map.sheetNote and list.caveat.row in all 11 locales: a cluster is of
people-group pins, not centroids, and each pin marks a people group, not a town.
- The key-cities and languages-per-city caveats in build-tabulator-tables.mjs and in every committed
copy of those two tables (only that one field changes), derive-city-candidates.mjs's caveat, and
the matching comments in key-cities, name-clusters and the poster generator.
- check-approx-pin-note's pass line no longer calls pins localities. Two UI-strings baseline entries
whose strings no longer exist in their files are dropped.
A poster plugin now lists its designs in its own <profile>.meta.json under
posterDesign ({ family, name, size, mobileFriendly, labelKey?, instance? },
one or an array). internal/profile-meta.mjs carries the block into
manifest.json only when present, so every existing manifest keeps its bytes
(the built and the dev manifest share that function).
Poster Design's Styles reads the tree's manifest at runtime and gives each
declared design a card under From other plugins: the card says it is made by
its own plugin, and Show full details shows that plugin's page (or preset
page) live, with a link to open it. No plugin code is imported, and a plugin
adding its block does not rebuild Poster Design.
check:poster-design-catalogue now also validates every block, keeps names
unique across templates.js and all blocks, and keeps a names ledger
(src/catalogue-names.json): a shipped name that is renamed or removed fails.
Its new self-test (in gates:pre) proves an invalid block, a name clash, a
renamed ledger name and a missing preset each fail, and the documented
example passes.
Seventeen lines in 15 files. Six quoted short fragments from feedback; eleven used "design note" where a
person once stood ("design note selects 2+ rows", "a design note design call"). Each now states the
reason in neutral terms and keeps the technical content and dates.
All edits are comments, except one string: the safety_note text in
library/scripts/research/derive-city-candidates.mjs, a research script that is not bundled and whose
output is not committed. No logic changes.
- ONE REGISTRY PER PAGE. Every bundle inlines its own copy of posterTypes.js, and <dxm-poster-section> is
defined by whichever bundle loads first, so with a module-level Map that element saw only its own bundle's
types: type="upg100-list" rendered "none" when the country-posters bundle had defined it. The registry now
lives on the page (globalThis.__DXM_POSTER_TYPES__, created once): every copy registers into it and reads
from it; a name registered twice keeps the first (a dev warning; the built-in country is silent); every
registration is announced on window (dxm-poster-type-registered), and PosterSectionView looks again when
its type arrives after it asked.
- COLUMNS (carried from the Poster Sections plugin PR so the element's library lands once): columns="1".."5"
maps to profile-config.columns and to PosterWall's existing columns prop; absent or out of range is
today's default, and a reader's saved per-row choice still wins.
- check:poster-type-registry (gates:pre; self-test in gates:post): two independent copies of the module side
by side, as two bundles on one page; a type registered through one is found through the other; first wins;
announced. The self-test's per-bundle registry fails.
globalThis.DXM_PREVIEW_UNRELEASED lets a page show what is withheld from the public (the whole-India country
poster). defineMapElement.js, every element's first module, now sets it behind import.meta.env.DEV: true only
under the Vite dev server (the pilot's hot-reload preview), a compile-time false in every build, so the build
drops the line. The built tree, which goes to the research landing and DX Life's /dx-maps, never carries it.
check:preview-flag-dev-only (gates:pre; self-test in gates:post): no file under public/ assigns the flag in any
spelling a minifier writes, and every source assignment is behind import.meta.env.DEV (2 planted defects).
The gate compiled PosterWall.vue alone, as a client component, and runs
Vite without the Vue plugin. A child component imported by PosterWall
(a section component, which a refactor is about to add) would fail to
load, turning the gate red on correct code.
One resolveId/load hook (enforce 'pre', so Vite's resolver does not load
the raw SFC first) now client-compiles PosterWall and every .vue it
imports, each at its real path, so their own relative imports resolve as
in the build; the earlier '../../' path rewrite is gone. Arm L loads a
parent fixture that renders a child .vue by relative import
(internal/fixtures/poster-wall-host-seams/) and asserts the child's text.
It fails without the hook and without 'pre'.
- WEB MODE GETS THE TOP BAR BACK. The corner ☰ with no header was rejected on a real phone. Both shell modes
now show the one top bar on a phone, the ☰ at its start and the language at its end; web mode's bar takes no
camera inset (the shell is not at the top of the screen in a website). The corner circle and the drawer-foot
language are gone. check:shell-header renders both modes (13 planted defects).
- THE FLOOR NEVER EXCEEDS A SIZED HOST. ProfileLoader's 100svh floor is for a host sized by its content (the
scroll-page leaf). Inside a host with a definite height (DX Life's map slot, overflow hidden) it made the map
taller than the host, which clipped the map's bottom and its legend footer, and tied the map's height to the
window's. ProfileLoader now MEASURES the host (the floor at 0 and at a huge value: a sized host keeps its
height) and drops the floor inside a sized one, re-measuring when the host resizes (a ResizeObserver on the
host, not a window resize).
- LegendDesktop caps itself at the map's height (100% - 80px), not the window's (100vh - 80px).
- On a phone the drawer started OPEN (navOpen = initialNavOpen) and mounted() closed it, so every load drew it
on screen and slid it away (measured: up to ~1.6 s, both shells, app and web mode). data() now reads the
same (max-width: 760px) query, so the first render is already the phone's closed state; mounted() still
tracks breakpoint crossings.
- In web mode the language at the drawer's foot sat right under the last row: margin-top:auto needs the nav's
flex column (.has-rail-end), which only the desktop rail-end set. The phone drawer's foot sets it too.
- check:shell-header asserts the flex column in web mode (14 planted defects).
ONE HEADER ON A WEBSITE. Inside a host site that has its own header, the shell's 44px phone bar stacked a
second white bar under it. VerticalTabsLayout gains shellMode (prop, or the host element's shell-mode
attribute): web | app | auto.
- app (a Capacitor or standalone app, or a DX Maps leaf page): the phone bar, unchanged.
- web: no bar. The rail opens from a 40px corner ☰ at the pane's bottom-start, which publishes its footprint
(--map-fab-clearance / --map-fab-beside) so the legend sheet, the pin hint and back-to-top step aside; the
language menu moves to the drawer's foot.
- auto: the host's shell-mode attribute, else app for Capacitor, display-mode standalone or a leaf
(window.MAP_OWNS_PAGE), else web.
Desktop is unchanged. check:shell-header renders both modes (13 planted defects); check:nav-reachable-mobile
accepts the corner ☰.
MAPS HOLD THEIR SIZE. ProfileLoader floored every map at min-height 100dvh, and the leaf pages' slot did the
same. dvh follows the mobile URL bar, so scrolling grew every map mid-scroll and moved its legend sheet. Both
now use 100svh (the viewport with the bar showing, which never changes on scroll), with 100vh where svh is
unsupported.
On a phone the first-visit PinHint toast sits over the HUD's bottom edge, and the
message line under the buttons was covered there. A hit-test across the text
found the toast at 30%, and the screenshot read "This needs a" and no more.
The line (Copy's flash, the phone ceiling's notice, failures, the print's
progress) now sits under the hint at the top of the HUD, at 11px instead of 9px.
check:batch-selection-export pins it above the action rows.
.upg-status is display:none below 768px (the legend sheet owns the bottom edge),
so the phone ceiling's "This needs a desktop or laptop computer." and the print's
progress ("fetching people groups…", "printing 905 people groups…") were written
only where a phone shows nothing. A reader tapped Prayer List and nothing visible
happened.
hudSay writes a batch action's message to the batch HUD's line (.upg-hud-flash,
now a polite live region, visible at every width) as well as to the status
badge. The block notice stays 6 s and failures 5 s. The print's progress is
mirrored into the same line whenever no flash is up, and is cleared when the
print ends.
check:batch-selection-export arm C checks that the notice, every batch action's
failure text and the progress reach the HUD line, that no batch action writes
via flashStatus alone, and that no rule hides .upg-hud-flash. Four mutants each
fail, including "notice written only to .upg-status".
Building a selection's Prayer list or Card deck is never the problem (the whole
selection is ready in under 30 s on a 4×-throttled phone). PRINTING it is: in a
browser capped at 2.5 GB, the print of 1,857 groups (101 sheets, a 138 MB PDF)
finished, and 2,209 groups (113 sheets) crashed the page at 1× and at 4×. A phone
tab has roughly half that memory, so above 1,000 groups (PHONE_GROUP_CEILING,
with the measurement beside it) a phone or tablet gets the house text
"This needs a desktop or laptop computer." (poster.phoneLimitAll, as Country
Posters shows for whole India) and nothing starts. India alone (905) still
prints on a phone. A desktop is never blocked. Copy and CSV stay allowed.
The device test is isPhonePrint(), the same platform test the print path uses.
check:batch-selection-export arm A7 tests the ceiling both ways. Arm C checks that
Prayer list, Card deck and Poster each ask BEFORE they print, and arm D checks
the string ×11. Four mutants each fail.
Measured on the 60x96 print: every photo beside a name rasterised as the browser's broken-image glyph. An SVG
rasterised through <img> is an isolated document that fetches no external resource, and none of the people-group
photo hosts (media.joshuaproject.net, joshuaproject.net, images.peoplegroups.org and its resizer,
peoplegroupspublic.blob.core.windows.net, s3.doxa.life) sends CORS, so a photo cannot be inlined as data: in the
browser either.
- poster-raster.js: rasterPoster drops every <image> whose href is not data: (the basemap tiles are data: and stay),
so a download shows the paper, never the glyph. hasExternalImages() says when a sheet carries such photos.
- PosterWall: printing a generated sheet that carries photos prints THE SHEET ITSELF through printDocument, where
the photos load, on its own @page (60in 96in), instead of a raster shrunk onto letter. Same HUD steps.
- check:side-lanes: photo arms (0 external images reach the raster; the wall prints such a sheet as a document),
each with a poison in its self-test.
- check:side-lane-photo-pixels: the pixel side, a browser gate (a data: photo draws red; 99/99 external slots stay
flat; without the strip 0/99 do). Declared on-demand until its first browser-seat run.
DX Life's /pray at 1440 kept the pill at the viewport's bottom until the map's bottom came into view: the page
scrolls a container, not the document, so the document-only test saw a full-screen page and kept the fold.
pageScrolls now also walks out from the pill, across shadow roots, for an ancestor scroller whose content is
taller than it; a map wholly below or above the viewport is treated as on a scrolling page; and a lift is
cleared when the pill goes off screen, so a lift set while the map was half visible does not survive into the
next view. check:pin-hint-floor adds the inner-scroller, shadow-crossing and map-below-the-fold fixtures; its
self-test refuses 9 planted defects.
Folds in the review's four points:
(a) check:pg-name-gate plants a row with EXACTLY two far Latin locales, which must
show English, so a rule that needs three fails.
(b) A row with fewer than two Latin names cannot be checked (117 rows), so it now
shows English too. The whole table: 8,172 of 21,419 (38.2%).
(c) The one-reader scan also flags a file that imports the helper but still
indexes rows itself (`.names[`), with negative controls both ways.
(d) The helper documents that callers always pass `english`.
Seven mutants of the helper each fail the gate.
The packet and card-deck title named the selection's zones and countries only.
Since India's states bring people, a selection of states alone had no names, and
reduce() threw "Reduce of empty array with no initial value" at the reader.
selectionTitleNames (data/selection-export.js, pure) adds the states and union
territories that brought people. A province with no table is not named. A
selection that brought nobody (Mizoram or Nagaland alone) gives the keyed
"no people groups to print" instead.
check:batch-selection-export arm A6 tests it on the real served table: Gujarāt
alone is titled "Gujarāt", a state + a country names both, a province beside it is
not named, and Mizoram alone reads the keyed text. Its negative control shows the
old naming is empty and reduce() throws. The call site is pinned in arm C. Four
mutants each fail.
library/utils/peopleGroupName.js (pure) returns a group's name in the reader's
language only when the row's names are transliterations. When two or more of its
Latin-script names (fr pt es tl id de) are far from the English one (bigram Dice
< 0.5), the row was machine-translated as words (Balantak → "Balance",
Balo → "Sac à dos"), and every locale of that group shows English. That is
8,055 of 21,419 rows (37.6%). it and ro have no column and show English; zh
reads zh_CN. A fallback shows the caller's own English name.
check:pg-name-gate (gates:pre) proves it on the real table: the 4 known
translated rows show English in 7 locales, Balangao Bontoc keeps its names,
planted rows are two-sided, the whole-table fallback stays between 30% and 45%,
and no file outside the helper reads pg-names rows (pplr-data-maps is the
exception; the scanner is negative-controlled). Five mutants of the helper each
fail it. No consumer is wired yet.
A sweep brought tracked source to 0 such lines; this gate keeps it there. It scans every tracked text file
except research data, build output and the translation catalogues, and allows only named lines (a file plus
the exact line, with its reason), never a whole file; the list is empty, since the gate builds its own
patterns from parts. gates:pre runs it; gates:post runs its self-test, which plants each class and checks
the per-line allowance.
The Country Posters India section headed "855 people groups · 37
countries", which is untrue: its cards are India's own and one per state
and union territory. sectionItemCountKey (default null: every section
counted with item-count-key, as before) lets a wall answer, per section,
an i18n plural key, or { key, n } to count n rather than the section's
card count, or null to keep the default.
Walls that do not pass it render byte-identical trees before and after
(a differential render of an upg100-style and a country-posters-style
wall). check:poster-wall-host-seams arm K: a named section counts in its
own key and number, an unnamed one keeps the default; it fails with the
prop ignored.
A section head read "<n> people groups · <items>" always, so a wall whose
cards count no people (Poster Design's gallery of templates) showed
"0 people groups · 4 lists" on every head. The total and its " · " now
leave together when the total is 0, the rule the cards already follow with
v-if="item.count". The head is still one string in one interpolation, so a
section with a total renders exactly the text it always did, whitespace
included: a differential render of an upg100-style wall and a
country-posters-style wall, before and after, gives identical trees.
check:poster-wall-host-seams gains arm J (a head with people keeps its
total; one without reads its items alone); the old head line fails it.
The wall intro writes its total in the digits the translated sentence already uses
(countInSentence): ar shows ٢٬١٠٧ beside its own ٢٧ and ٩:١٤, while CLDR's bare
'ar' would have formatted 2,107. Locales whose sentence uses Western digits are
unchanged. The fetching status goes through the same helper.
A people-group load that fails without a catalogue key (the 30 s abort, a dropped
network) now reads as the stall text, "still loading, try again in a moment",
instead of the browser's own English ("signal is aborted without reason"). Keyed
errors keep their own text, and the failed load leaves the cache, so the next Copy
fetches again.
check:upg-count arm E renders the intro through each locale's catalogue and
requires one digit system, with a negative control on the old ar render. Its
separator scan now also reads fr U+202F and ru U+00A0. check:batch-selection-export
arm A5 aborts a real fetch and refuses a real connection; both must read as the
stall text, and the old path must still show the raw message.
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).
On a partner home page the map is a block in a scrolling page. The clamp floored the pill at the viewport
fold, so a map whose bottom was below the fold at load had its pill lifted to mid-map, re-set by the settle
timers and resize observers, and left there after a scroll, then dropped. The fold now counts only on a
page that does not scroll (a full-screen pane or leaf, where a phone's browser bar can cover the bottom);
the legend sheet and the licence credit still lift the pill everywhere. The floor is a pure function
(pinHintFloor.js), and check:pin-hint-floor proves both cases; its self-test refuses 5 planted defects.
Every size up to 36x66 prints on one sheet; 66x66 is two 36x66 sheets glued side by side over 6 in, and 60x96
is three 36x60 sheets stacked over 6 in overlaps. 36x48 moves from legacy into the ladder (3x4ft still
resolves to it); 24x36 stays legacy. check:poster-sizes now also refuses a size that is both offered and
legacy, and a glued size other than 66x66 and 60x96; its self-test refuses 10 broken tables.
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.
VerticalTabsLayout, header off: on a phone a 44px bar (a real grid row) replaces the floating corner
circle, with the rail toggle at the inline start and a #phone-end slot at the end; on a desktop a
#rail-end slot sits at the bottom of the rail (it receives `collapsed`). Neither renders with a header
on, so header-on consumers and desktop layouts without a filler are unchanged. The --map-fab-* footprint
is no longer published; its readers fall back to their plain insets.
ShellLanguageMenu fills either slot: the switcher's languages plus the current one, a native select
under a globe face. A pick dispatches one cancelable, composed, bubbling doxa-language-request on the
host element (library/utils/languageRequest.js). A host that cancels keeps the language; otherwise the
menu emits 'switch' and writes ?lang= only on a page the tree generated.
check:shell-header (gates:pre, Vite SSR) asserts where the bar and the rail slot render, the event's
shape and count, cancel and no-cancel, the ?lang= rule and the language list; its self-test (gates:post)
refuses 11 planted defects. check:nav-reachable now presses the bar's toggle.
library/constants/posterSizes.js gains POSTER_LADDER: five portrait sizes keyed WxH in inches, each with the
sheets a printer feeds (named as panelSheets.js planPanels() takes them), whether a photo slot fits, and how
it reaches paper (the browser's print for paper, a print-shop PDF for the rest). Legacy 24x36 and 36x48
still resolve, older names (3x3ft, 5x8ft, letter, a4...) alias to the ladder, and paperFor() picks Letter
or A4 from a locale's region. The existing map-export presets are unchanged.
check:poster-sizes (gates:pre) audits the table: the order matches the ladder, keys are dot-free, every
size is portrait, the sheets glue up to the finished size, photos on exactly the two large sizes, paths,
aliases and the paper fixtures. Its --self-test refuses 8 broken tables.
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.