Commit graph

12 commits

Author SHA1 Message Date
Doxa Maps
f5fa9e05b1 WIP packet: Mark 5:18-20 back on the points page (source) 2026-09-30 11:38:02 +00:00
Doxa Maps
2dbd567945 WIP packet 10:58 (the dictated points, point 4's statement + lead-in, the wheel turned with Start here; re-pinned; 5 bundles rebuilt): measured, reads pending 2026-09-30 11:17:25 +00:00
Doxa Maps
86034174b0 WIP Mark 5 page (source) 2026-09-30 09:32:33 +00:00
Doxa Maps
8105b1e76a Prayer packet: point 4 prints a short reading list, and the 12 points are larger
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.
2026-09-30 08:29:54 +00:00
Doxa Maps
cc3406cfe4 The UPG-100 list prints its packets through the shared lift, not a second copy of it
The list kept its own page-1 map lift (packetMapSvg + packetLabelFor) beside the one in
usePosterPrints, and the two drifted: the list's relabelled country names, the wall's did not, which
is the zh finding rega made in the app. With the composable now taking iso3Names, the list passes it
and deletes its copy, so the list, the wall and any other map print page 1 through one path.

The selection packet borrowed one list's sheet when it held exactly one list. That is now a general
seam: item.mapId names the sheet when it is not the item's own id, and mapId: null says the item has
none, so a multi-zone selection fetches nothing and page 1 says the map is missing. Measured two-sided
through the real printPacket. gates:pre 66/66.
2026-09-29 05:26:23 +00:00
Doxa Maps
2367c2ef64 Build the prayer packet page by page to its specified structure
FULL DETAIL is four front pages, the profiles, four back pages:
  map, triangle, life calendar, % evangelical, profiles,
  bounded set, centered set, prayer wheel, 12 prayer points.
LIGHTWEIGHT is two front, the profiles, two back:
  map, triangle, profiles, prayer wheel, 12 prayer points.
Both are generated from data, in every packet language, from the
browser and from the offline generator.

PAGE 1 - the map, on a vertical page. One lift for every caller
(packet-render.js liftMap): the browser composable, the UPG 100
profile and the offline generator each carried an identical copy of
the old wrapper (250px down, scale 0.62), and three copies of a layout
decision is how page 1 drifts between print paths. The lift prefers
the map-only vertical sheet (mapSheetFiles: <zone>.packet.html, then
the poster) and places it so its names land on the same 60px margin
every other page keeps; a poster falls back fitted under the title at
0.81 rather than 0.62. The title block is drawn after the map, since a
map-only sheet begins with a full-page background. Anything that is not
a map sheet - an icon-sized <svg> from a host that answers a missing
file with a 200 page - is refused, and page 1 says the map is missing.
Page 1 now states which set it belongs to ("One of 28 UPG 100 prayer
lists"), only when the caller supplies the count.

PAGE 2 - one inverted triangle in the Vision Card's look. The card's
steps are the top (every people group, the unreached, the frontier,
the network's list), narrowing to the UPG 100 packets and, at the tip,
this packet. The card's greens, darkening as it narrows, walked
through its own colour stops; every numeral picks white or ink by the
WCAG ratio (all at least 3.4:1, large bold type). Tier widths are
geometric, not proportional - 2,107, 28 and ~100 against 12,379 would
be hairlines - so where two sets truly nest the caption states the
step removed, and the network's list, which does not sit inside
frontier, gets the card's "N of them are frontier" note instead of a
false subtraction. The packets tier and its count appear only when the
caller supplies one.

BACK PAGES - the bounded and centered sets get a page each at twice the
width, under the one page key, so lightweight drops the pair together.

THE WHEEL - the Metacamp Module 1 prayer wheel: its twelve colours,
sampled from the fill operators in the source PDF's content stream; its
orientation, the opening PRAISE at nine o'clock and READ THE WORD at
twelve; white labels with a shadow and a faint outline, so a print
path that drops SVG filters still leaves them legible on the palest
segments; "5 minutes" badges; the attribution with the website. The
labels stay the packet's own rhythm names, translated in every
language and matching the 12 points page.

The offline generator makes both packets (PACKET_VARIANT=lightweight;
the full packet keeps its names, lightweight is <zone>.lightweight and
stamped with its variant).

STRINGS: four new keys, in all eleven blocks - each block's own words
for "packet" and "prayer list", the frontier note aligned to the word
the frontier tier uses on the same page.

GATES: check-packet-a1-structure (registered in gates:pre) reads every
sheet's CONTENT for the order of both variants, page 1's lift in all
four cases, the triangle's tiers and steps, the wheel's colours and
orientation, and all 28 zones. Mutation-tested eleven ways, each caught
by its own message. check-packet-variants now asserts both sets pages
and a four-sheet difference. The browser gate check-packet-render
counted funnel rows and bars by inline-style substrings, which an SVG
triangle has none of - both would have read zero and passed - so it
now counts .funnel-n against .funnel-tier, its sheet arithmetic counts
the sets pages, and its Latin allowlist carries the renamed product
name. check-packet-browser used a copy of the old wrapper; it now uses
the product's lift.
2026-09-29 02:16:28 +00:00
Doxa Maps
fbc517c94a A browser gate refuses to launch outside the browser slot; check:browser-gates-guarded keeps it that way
Only the browser seat runs browsers, and only inside internal/browser-slot.sh.
That rule was remembered, not enforced: an agent ran a gate it took for a
headless source check and it launched Chromium outside the slot.
internal/lib/playwright-guarded.mjs is now the one way a repo file reaches
playwright: its launch(), launchPersistentContext(), launchServer(), connect()
and connectOverCDP() exit 75 (NOT MEASURED) with a message naming
browser-slot.sh unless DOXA_BROWSER_SLOT is set, and browser-slot.sh exports it
for the command it runs, so a bash -c grandchild inherits it. All 72 files that
imported playwright switch to it (internal/lib/browser.mjs and 64 internal gates;
plugin-designs scripts: doxa-research-map 3, my-upg-100-list 2,
research-map-plus-grid 2, research-dashboard 1): a mechanical import swap.
check:browser-gates-guarded (gates:pre) fails a direct import;
bun run browser-gates lists the 97 scripts that launch a browser. isBrowserGate
moves to internal/lib/browser-gates.mjs so the runner and the check share it.
2026-09-28 08:20:21 +00:00
doxa-maps-88655c70
9acd4f8865 Queue the browser gates, and let them serve their own tree
Two problems, one change.

LOAD. Seven agents each running a wave put 7+ chrome-headless renders on one box;
measured today it reached load 47 with 8GB of swap, and a browser gate that is
swapping measures the swap, not the product. The wave runner now holds browser
steps while 1-min load is at or above LOAD_MAX (12) and takes a coarse flock
before each one, so they queue instead of dogpiling. Thresholds, cadence and the
20-minute cap are bundler-3's wait-for-quiet.sh verbatim, and BROWSER_GATE_LOCK
selects the rendezvous, so every agent lands on one lock whichever entry point
they use. Static gates are untouched: in gates:post that is 38 free, 4 queued.

The load wait is taken ONCE per wave, not once per gate. Per-step it multiplied
the same hold by the number of browser gates — an hour of a wave doing nothing,
which is how a load fix becomes a reason to stop running gates. The flock is
what serializes; the wait only decides when to join the queue.

Browser steps are identified from the gate FILE (a playwright import), not a
hand-kept list that goes stale the first time someone adds a gate — the one
missing from such a list is exactly the one that rejoins the dogpile. `verify` is
caught too: it imports no playwright itself but spawns three gates that render,
and it already marks them browser:true, so that declaration is honoured rather
than duplicated.

COVERAGE. Yesterday's guard made check:provider-switch and check:packet-browser
SKIP in every wave, because their default ports (4173, 4898) belong to other
worktrees on this machine. Skipping was honest but a gate that skips every time
is a gate nobody runs, so on a mismatch they now start a short-lived server over
THIS checkout and measure that instead — the point was never the port, it was
being able to prove which build answered.

MEASURED: a served tree whose manifest differs by one key but has all the same
pages -> "serves a DIFFERENT build", then "serving this checkout on :28384
instead", then a real verdict (16 -> 5 mapbox requests). packet-browser likewise,
once it re-navigates to the new origin — a dynamic import across origins is
refused by a plain static server, which cost one failed run to learn.
gates:post 40/42, the only skips being the two that genuinely have no input
(packets-all, zone-census); it was 4 before this.

The lock path default is product-named. The shared file several shells export
carries an internal code name, and check:no-codename refuses to let a repo we
hand over carry one — it caught this commit's first draft.
2026-09-24 17:43:32 +00:00
doxa-maps-88655c70
6602cd1399 Browser gates: prove the server is THIS build before believing it
Five gates took their measurement from whatever answered a default localhost
port. On this machine that port belongs to whichever worktree started first:
:4173 is wt-packet's server, :4898 is its repo root. Measured today — an hour of
probing reported on a build that was not on disk here, and the mistake was only
caught because a marker string was in the file and absent from the response.

"Answers 200" and "is serving this checkout" are different questions, and only
the second one licenses a verdict. _serves-this-tree.mjs already asked it (it
exists because the gospel-resources gate hit this and spent three runs reporting
on the wrong tree) but only three callers used it. Wired into the five that
measure a served page and had no such check:

  check-provider-switch        request tally per host — the wrong tree's tally
  check-maplibre-layer-drops   silent layer drops — invisible by nature
  check-filter-width-stable    width divergence — would accuse untouched CSS
  check-facet-changes-map      data flow on facet change
  check-packet-browser         imports the renderer FROM the server

Only a genuine MISMATCH skips. "No manifest to compare" is a different
precondition (an unbuilt checkout) that each gate already speaks for, and
turning that into a skip would silence these on the builds they should run on.
A mismatch exits 0, like the unreachable case, because it is a precondition and
not a product finding — but it prints which, so nobody reads it as a pass.

CONTROLS, each fired, per gate: correct server -> PASS with a real number
(provider-switch 19 -> 5 mapbox requests, drops 17/17, filter-width identical,
facet 2107 -> 1209 dots, packet-browser headings match); a served tree whose
manifest differs by one key -> SKIP naming "serves a DIFFERENT build"; no server
-> the pre-existing unreachable SKIP, unchanged.

One correction found by running it rather than by reading it: the first wiring
passed the LEAF PAGE URL where the helper wants the ORIGIN, so it looked for the
manifest under the page's own path and skipped every run for the wrong reason.
node --check cannot see that; only running both controls can.

KNOWN COST, not hidden: in a wave with no server of this tree running,
check:provider-switch and check:packet-browser now SKIP where they used to
report a green about another worktree. That is the honest reading, not a
regression, and the runner prints "evaluated nothing" for them. Next increment
is to let these gates serve this checkout themselves so they measure again.
2026-09-24 17:00:11 +00:00
doxa-maps-88655c70
7a649b6845 packet browser gate: verify the packet a reader actually exports
Some checks are pending
build-gate / build (push) Waiting to run
The legend's Export > Prayer packet action is the path a reader clicks. The
build-time file is the one nobody clicks. The reader's packet was the less
verified of the two.

check:packet-render gained six assertion families over recent ticks — the 12
prayer points, the wheel's 12 LABELS rather than just its 12 segments, page 1's
geography, the print layer, the image census, the build stamp. Each was found by
mutation and each catches a real "looks fine, is empty" shape. None existed
here.

It also printed "Mark 5 in full : undefined" beside a PASS for as long as it has
existed: the measurement is r.mark5Len and the print read r.mark5, which is not
a key on r. The assertion always read the right key, so the requirement was
protected — but undefined next to a PASS reads as a gate that does not know what
it is doing. That is the inverse of this tree's usual failure, where a number is
printed and never asserted on.

A mistake caught before it became a false report. The new geography assertion
went red immediately with "no map svg on page 1", which would have read as a
serious product defect. It was the harness: this gate called buildPacket with an
EMPTY mapSvg, while the real export path fetches posters/<zone>.html and lifts
its <svg>. The shortcut was not just skipping an assertion — it meant the gate
never exercised mapCss() in a browser at all, which is the one thing it exists
to prove. It now fetches the map the way the product does and reports 13
geography paths, the same number check:packet-render gives for laos. ISO3 and
ZONE are also different strings ("lao" vs "laos.html"), so even a naive fetch
would have missed; PACKET_ZONE names it.

Mutation-tested: blanking the wheel labels gives 0/12 labelled and fails; hiding
content at print time gives 0 embedded fonts and fails.

i18n verified on the browser path for the first time. A partial locale with 3 of
12 points renders 12/12 points — the per-key merge holds here too — with the
points heading translated, the wheel heading falling back to English, Mark 5 at
2220 chars, and nothing blank.

Also made wave-safe: without a server it threw an unhandled navigation error and
dumped a stack trace, which is why it could never join a wave. It now SKIPs
cleanly naming its precondition. gates:post 35 -> 36, README re-synced. Verified
both ways — 36/36 with a server, and "0/1 passed, 1 SKIPPED" without one, which
is visible only because of the runner change in the previous commit.
2026-09-24 14:59:25 +00:00
doxa-maps-88655c70
3519ca3934 check:packet-browser asserts its headings, and stops reading them in English
This gate printed nine metrics, folded six into `const bad = a || b || ...` — so a
failure said "FAIL" without naming which — and left three unchecked: page size and
the two headings, which are the very strings its own header claims it covers ("the
gate reports which headings came back translated"). Reports, not asserts.

★ IT LOOKED LIKE IT HAD FOUND A PRODUCT BUG, AND HAD NOT. Under ?lang=es it printed
an EMPTY wheel heading; under hi it blanked BOTH — while the catalog holds "Rueda de
Oración" and "प्रार्थना चक्र". That reads exactly like a broken i18n path, and it
PASSED anyway, because nothing asserted the values.

The cause was the harness. wheelTitle was innerText.match(/^.*Wheel.*$/m) and
pointsTitle /Prayer Points|Puntos/ — neither can match Hindi, and /Wheel/ cannot
match Spanish. The product was correct in every locale all along: verified after the
fix in en/es/hi/ar/zh, RTL and Devanagari included. Had I reported that as a defect
I would have been wrong.

mark5 carried the same fault: innerText.includes('Gadarenes'), an English proper
noun that goes RED the moment point4_passage is translated. Now asserted by
.p4-passage length — the same fix already applied in check-packet-render.

Headings are now compared to the CATALOG entry for the packet's language, with
per-key fallback to English exactly as the renderer resolves it. packet-render.js
gains class="points-title"/"wheel-title" as TEST HOOKS: both were inline-styled with
no selector, which is what forced the English greps in the first place.

MUTATION-TESTED: a renderer forced to ignore the language FAILS with both headings
named and the expected values shown; a renderer emitting an empty heading FAILS
("a blank heading is worse than an English one").

★ ONE MUTATION CORRECTLY PASSED, and it marks a real limit: reverting es
wheel_title to English IN THE CATALOG passes, because rendered and expected then
legitimately agree. This gate cannot tell a translated catalog from an untranslated
one — that is check:packet-strings-parity's job, and it does assert it. Two gates,
two questions.

Partial translation re-verified through the BROWSER path: a 1-of-21-key Swahili
block renders "Mambo 12 ya Maombi" with the wheel heading falling back to "Prayer
Wheel" — translated where present, English where not, neither blank.

That makes four gates with one root cause (tabs-localized, packet-render,
deck-click, and this one twice). Any new gate matching on user-visible text should
resolve it from the catalog.
2026-09-24 10:50:27 +00:00
doxa-maps-88655c70
097ab02458 test(packet): gate the packet the READER gets, not the one Node writes
Some checks are pending
build-gate / build (push) Waiting to run
check:packet-render verifies a packet FILE from the build-time generator. This
verifies the BROWSER path — the legend's Export > Prayer packet action, which
imports library/packet/packet-render.js, fetches live records and prints
in-place.

Both callers now share that one renderer, and this gate is what proves the
shared module actually runs in a tab. A module can be perfectly correct under
Node and still fail in a browser: a bare process.env reference, a Node-only
import, a JSON import assertion. Node passing says nothing about that.

It drives the real thing: the renderer over HTTP, the app's own normalizer
(the API returns { posts, total } — hand-rolling that extraction is how a probe
ends up testing something the product never does), live records, then
page.pdf() and a PDF page count.

Measured on the browser path: 8 sheets -> 8 pages at 612x792pt, 95 people
cards, 12 wheel segments, Mark 5:1-20 in full, exactly 2 metacamp.org
attributions, 0 page errors.

I18N is asserted here because it is a product requirement, not a nicety: pass a
language and the gate reports which headings came back translated. Verified with
a partial Spanish block — "12 Puntos de Oración" and "Rueda de Oración" rendered
while Mark 5 and both attributions correctly fell back to English. Fallback is
PER KEY, so a half-finished translation renders its finished keys and never
blanks. The stub block was removed after verifying; shipping it would imply a
Spanish translation that does not exist.

Parameterised rather than pinned: PACKET_ORIGIN / PACKET_API / PACKET_ISO3, and
the language as argv. Needs a static server over the repo root.
2026-09-24 02:44:06 +00:00