Commit graph

2672 commits

Author SHA1 Message Date
Doxa Maps
a86b3d7bee The map controls and the pin hint printed; the attribution stays
rega printed a full-bleed map leaf on fc93c97e4 and the release worked — 1 page,
canvas 1440x900, map drawn, floor held. Two things printed that have no business
on paper, both found only because someone looked at the sheet.

The control column — zoom in, zoom out, locate, fullscreen, share — printed top
right, over the map it exists to operate. Every one is a pointer affordance
with nothing to do on a page. The engine's own control groups go with it, for
maps that place them outside this toolbar.

The pin hint printed across the bottom: "Tap a coloured dot — someone still
needs prayer". It is an instruction for a finger, addressed to a reader who is
now holding a sheet of paper. Worse than useless there, because it sits under
the map like a caption and says something untrue about it.

The ATTRIBUTION is deliberately left alone. It looks like chrome and it is not:
it is a licence term of the basemap, and a printed map that drops it is a
licence problem, not a tidier page. Only the attribution TOGGLE goes, which is
the button that expands it.
2026-09-28 20:54:26 +00:00
Doxa Maps
988092d363 Release every window global this profile sets, on unmount
Some checks are pending
Deploy to GitHub Pages / build (push) Waiting to run
Deploy to GitHub Pages / deploy (push) Blocked by required conditions
A window key holding a closure pins the whole app after unmount -- the
component, its shadow root, and everything they reference, measured at
roughly 10 MB per mount. The three test handles here each close over live
state (the search index, the map, the selection), so each one is a full
retention path.

SIX GLOBALS, NOT THREE. The check names the three it can see, but each
named handle has a paired per-instance registry --
__UPG_SEARCH__/__UPG_SEARCHES__, __UPG_MAP__/__UPG_MAPS__,
__UPG_SELECT__/__UPG_SELECTS__. Releasing only the named half leaves the
registry holding the same closure, so the leak survives in the copy nobody
was looking at.

The two halves need opposite handling, which is why this is not one loop.
A named handle is a single slot that the last mount overwrites, so
deleting it blindly would strip a LIVE sibling's handle -- it is cleared
only if it is still the object this instance published, captured at
assignment so it cannot drift from what went out. A registry entry is
keyed by instance, so my key is always mine to delete, and deleting the
whole registry would strip the siblings.

Verified against the two-instance case rather than the single-mount one:
A mounts, B mounts and takes the named slot, A unmounts -- B keeps its
handle and its registry entry, and after both unmount nothing is
retained.

One delete per line, deliberately: the check reads these statements, so a
loop over a key list would release correctly and still be reported as a
leak. The whole block is wrapped because a frozen or cross-origin window
must never break teardown.
2026-09-28 20:46:53 +00:00
Doxa Maps
114075f9ab Rebuild country-posters pplr-data-maps, and regenerate the tree
Some checks are pending
Deploy to GitHub Pages / build (push) Waiting to run
Deploy to GitHub Pages / deploy (push) Blocked by required conditions
2026-09-28 20:41:27 +00:00
Doxa Maps
c9c2280241 Pplr Data Maps: its verification hooks leave window when a view closes, so a closed view no longer keeps the whole app in memory 2026-09-28 20:40:50 +00:00
Doxa Maps
401b5d28d0 Country Posters: Export poster opens its dialog again, and two legend calls that did nothing are gone (the legend's rows are called items) 2026-09-28 20:40:50 +00:00
Doxa Maps
a035408e26 Rebuild the served trees after releasing the window globals
Some checks are pending
Deploy to GitHub Pages / build (push) Waiting to run
Deploy to GitHub Pages / deploy (push) Blocked by required conditions
2026-09-28 20:33:53 +00:00
Doxa Maps
24b32e199f Release the test hooks on unmount, all twelve of them
Twelve globals were set across three plugins and none was ever deleted. Each
pins whatever it closes over — a map instance pins its canvas, its sources and
through them the whole app — so every mount left another copy behind, measured
elsewhere at roughly 10 MB each.

Released as a set rather than one at a time. A block that sets several globals
and clears the obvious one leaks the rest silently, and the obvious one is
whichever the author happened to remember.

Identity-checked, which is why the value is recorded as it is set rather than
cleared by name. Two instances can share a page, and clearing by name alone
would let the second unmount delete the first's live hooks — trading a leak for
a harder bug in which a live map loses its test surface at a distance.

A first attempt compared against a name that did not exist. Inside the try/catch
that guards these, the ReferenceError was swallowed and every delete silently did
nothing — a leak fix that looks exactly like a leak fix and leaks. The guard that
makes the assignment safe is the same guard that hides its failure, which is the
argument for recording what was set rather than inferring it at teardown.
2026-09-28 20:32:59 +00:00
Doxa Maps
82b2e2aa5a Call the layer setup unconditionally, and stop the figure being crushed
Some checks are pending
Deploy to GitHub Pages / build (push) Waiting to run
Deploy to GitHub Pages / deploy (push) Blocked by required conditions
The guard added last time is what kept this broken. createBoardMap resolves only
after awaiting the load event, so by the time the component runs, load has fired.
The guard asked map.loaded() — which does not mean "the load event fired" but
"the style and every tile are loaded" — and the basemap tiles were still
arriving. It returned false, took the else branch, and registered a handler for
an event that had already happened. A check added for a hypothetical future
engine broke the only case that exists.

Defensive code that changes behaviour is not defensive. If a check is ever wanted
here it is isStyleLoaded(), which is true at this point; loaded() answers a
different question than the one being asked.

The figure was crushed rather than unstyled. The zero flex-basis shipped
correctly and the figure was still 39 pixels wide: a basis of zero lets it grow
from nothing, but min-width:0 also lets it be squeezed to nothing, and the
decoder card's own content width wins that contest. The figure now refuses to go
below a usable size and the card shrinks or scrolls instead, because the chart is
the main view and the legend explains it.
2026-09-28 20:24:29 +00:00
Doxa Maps
182611c94b Regions batch: perRow boot, control inset, stats grid, print, two leaks, smaller watermark
Some checks are pending
Deploy to GitHub Pages / build (push) Waiting to run
Deploy to GitHub Pages / deploy (push) Blocked by required conditions
Seven changes to two plugins, re-applied onto current main as ONE source patch
rather than rebased, because the rebase conflicted in the generated bundles —
where a conflict is never resolvable by hand, since neither side matches what
either source builds. Source re-applied clean, everything rebuilt from it.

people-groups-grid
  · the saved column count is restored before first paint, not after, so a
    returning reader no longer watches the grid snap from auto to their choice.
    It re-reads if the profile-config key arrives late, and never over a choice
    the reader has since made by hand.
  · the cards-per-row control is inset from the card edge it was sitting level
    with (Driver round 2, #5).
  · the window vocabulary handle is released on unmount, identity-checked so the
    first of two grids on a page cannot strip the second's live handle.

regions-dashboard
  · the four stat cards no longer let Population stretch across a tablet row.
    auto-fit collapsed the empty tracks; auto-fill with the track minimum capped
    at a quarter of the row gives at most four columns and never a stretched
    orphan. Measured at nine widths.
  · the dashboard is printable at all. It had no @media print rule, so the app
    shell's absolute root and inner scroller meant the browser printed one
    screenful; the legend stayed on top of the map; interactive chrome printed.
  · both window globals are released on unmount, not just the remembered one,
    each identity-checked and on its own line.

shared asset
  · the worldmap watermark is 242 KB rather than 311 KB — same 394 paths, same
    drawing, coordinate precision trimmed to what the render resolves.
2026-09-28 20:20:50 +00:00
Doxa Maps
d9f8587ac4 Rebuild the served tree for the set-chart fixes
Some checks are pending
Deploy to GitHub Pages / build (push) Waiting to run
Deploy to GitHub Pages / deploy (push) Blocked by required conditions
2026-09-28 20:15:58 +00:00
Doxa Maps
2f26f61ea4 Draw the set charts at all, and give their figures room to be drawn in
Neither chart drew anything. createBoardMap already awaits the map's load event
and resolves after it, so attaching a second load handler waited for something
that had already happened: the layer setup never ran, each map held only its
basemap layers, and the panel showed an empty whiteboard.

It failed silently, which is why the visible-failure branch did not catch it.
Nothing threw — the map was fine, the source was simply never added. A guard that
reports errors cannot report an absence of work.

The setup now runs directly after the await, and still binds once('load') if a
future engine resolves earlier. That keeps the fix correct rather than merely
right today.

The figures also grow from a zero basis instead of from their content. The map
inside is absolutely positioned and has no intrinsic width, so a basis of auto
asks for a content size of nothing and the legend takes the column — the figure
was measured at 39 pixels across while the decoder had the rest. This was hidden
behind the first bug: an empty chart and a chart with no room look the same.
2026-09-28 20:15:38 +00:00
Doxa Maps
66e0697645 Say the table is loading, rather than drawing a confident zero
The neighbour table is 2.6 MB. Until it arrives the joined set is legitimately
empty, so both panels drew a chart of nothing and captioned it with a count of
zero — indistinguishable from a real answer of none, which is the same defect as
a join failure reported as a fact about a people.

Found by sweeping my own plugins for the Driver's bug classes rather than by
anyone reporting it, which is the point of the sweep: the newest code was the
code with the defect, because it had been read least.

The loading line is announced politely for a screen reader, since the change it
describes is a region of the page becoming real rather than an alert.
2026-09-28 20:15:37 +00:00
Doxa Maps
a0f5a9d8e4 Keep the reader's legend choice across a resize
Every width change reset the legend from the breakpoint, so opening the decoder
on a phone lost it on rotation, and closing it on a wide window reopened it by
dragging the edge. The resize was undoing the reader's decision rather than
responding to it.

The width now decides only until the reader does. After that their choice holds
at both widths.

This is the same defect fixed in the other plugin's legend earlier today, written
again here a few hours later, because the two legends are separate
implementations of one control and only one of them had learned. That is the
argument for promoting the control, not for being more careful next time.
2026-09-28 20:15:36 +00:00
Doxa Maps
7617c23f0c Rebuild the consumers main left stale (build owner, README rule 5)
Some checks are pending
Deploy to GitHub Pages / build (push) Waiting to run
Deploy to GitHub Pages / deploy (push) Blocked by required conditions
check:composite-freshness found stale on main: key-cities. Rebuilt, tree + SRI
regenerated; the landing precondition (fresh, one folder, SRI) passed in main-capture scan.
2026-09-28 20:09:10 +00:00
Doxa Maps
57c5416843 Wall of People Groups: words for the poster size, the poster's subtitle and its glue line, in every language 2026-09-28 20:01:40 +00:00
Doxa Maps
0608b57b94 UPG Neighbor: a name for the E-level filter, in every language 2026-09-28 20:01:40 +00:00
Doxa Maps
ed52654ff0 Wall of People Groups: words for the share notice and the drawing-library error, in every language 2026-09-28 20:01:39 +00:00
Doxa Maps
7b43a6bc10 Wall of People Groups: words for the bless banner and the mode switch, in every language 2026-09-28 20:01:39 +00:00
Doxa Maps
8da0853e44 Rebuild the consumers main left stale (build owner, README rule 5)
Some checks are pending
Deploy to GitHub Pages / build (push) Waiting to run
Deploy to GitHub Pages / deploy (push) Blocked by required conditions
check:composite-freshness found stale on main: key-cities
template-bundle. Rebuilt, tree + SRI
regenerated; the landing precondition (fresh, one folder, SRI) passed in main-capture scan.
2026-09-28 19:55:07 +00:00
Doxa Maps
afaaa85ef5 Assert the packet variant survives the trip from the menu to the renderer
This is the gate for the defect that cost the most: the packet dropdown
was built into a component the profile people open does not mount, so it
was landed, served, and invisible. Nothing caught it because every check
looked inside ONE file. The legend EMITS a string and the plugin PARSES
it, and those live in different files -- the only assertion that could
have failed is one that reads both and compares.

So it reads what the menu emits, then asserts the consumer can handle
every one of those kinds; that the kind is split on ':' rather than
matched against the two current literals, because matching literals is how
the next variant silently becomes a no-op; and that the parsed value
actually travels -- call site, signature, and the forward into printPacket
-- since asserting the split alone would pass while the value was dropped
on the floor. It also checks the five strings the menu renders exist,
because a missing key prints the raw key and the explainer is the only
place a reader meets what a UPG 100 list is.

Mutation-testing found a hole in my own assertion. The check for the call
site matched `printPrayerPacket(zoneId,\s*variant`, which the DECLARATION
-- async function printPrayerPacket(zoneId, variant = 'full') -- also
matches. A mutant that deleted the argument at the call site left the
declaration behind and the assertion stayed green: true for the wrong
reason, the same family as a tautology but reached through ambiguity.
Anchored on `await`, which only a call has.

Three mutants now fail with their own messages: the legend emitting a kind
the plugin cannot parse, the call site dropping the variant, and
printPrayerPacket holding it without forwarding it.
2026-09-28 19:41:30 +00:00
Doxa Maps
f172312cab Put the UPG 100 choice on the surface that actually shows it
The Prayer-list dropdown went into PosterWall's per-card print row. The
profile people open -- upg-100-list -- does not render PosterWall at all;
its print path is the legend's Export menu, which still offered two items
and printed immediately. So the feature was built, landed and served, and
invisible on the only screen it was asked for.

Two surfaces legitimately offer printing and both carry a button labelled
Prayer list, so the label was not enough to tell them apart. The grep that
would have caught it -- does this profile mount that component -- takes
five seconds and I never ran it.

The legend's packet item is now a disclosure: it is named UPG 100, it
explains what a UPG 100 list is, and it reveals Full detail and
Lightweight. The explainer matters because this menu item is the only
place a reader meets the idea, so it wraps rather than truncates.

The submenu opens BELOW its trigger by construction, not by positioning:
the menu is a flex column, so a block after the button flows underneath
it. A panel that grows its parent cannot cover its parent -- which is
exactly what the card-deck row could do, hanging out of its card at
top:100% with nowhere to go on a short viewport.

emitExport widens its existing string to packet:full / packet:lightweight
rather than adding a second event; poster and a bare packet still work, so
an older consumer keeps working and one event means nobody can handle only
half the menu. printPrayerPacket takes the variant and forwards it to
printPacket, which already accepts it.

UPG 100 is allowed to be identical in all eleven locales, recorded in
check:i18n-never-translated as a brand name rather than as debt: it is not
waiting on a ruling, it IS the ruling, and the explainer beside it carries
the meaning per locale. Inventing eleven names for one product would be
the failure that gate exists to prevent.
2026-09-28 19:41:29 +00:00
Doxa Maps
59d83ba6b3 The card-deck options clear the print row instead of covering its label
On mobile the size options sat on top of the words they belong to, so the
Card deck label disappeared behind its own menu.

The row hangs at top:100% and is then pulled back up by half the print
row's height. The reasoning was sound and the arithmetic is right at
exactly one line: the footer sticker half-overhangs the banner edge, so
half a row is the correct clearance. It stops being true the moment the
row wraps -- on mobile it takes two lines, and half of a doubled height is
no longer half a row, it is the labels.

A FRACTION OF A VARIABLE HEIGHT IS THE BUG. The sticker's overhang is a
constant, so the offset is now the full row height minus that constant,
which is correct at any number of lines. Measured at one, two and three
lines: the old rule overlapped by 10, 20 and 30px, covering labels from
two lines on; the new one overlaps by a constant 11px -- the sticker
alone -- every time.

Same shape as the box-sizing error on the packet's table rows: a value
that happens to be right at one size and is silently wrong at every other,
which is why it only showed up on a phone.
2026-09-28 19:41:29 +00:00
Doxa Maps
12d8c7a8d5 Rewrite the UPG-100 header so it opens with the question it answers
The old copy led with the instruction ("Hand away the prayer task in
clusters of fifties and hundreds") and then asserted "Each poster is a
100-list you can pray, print, and put on a wall", which does not read as a
sentence and never says why fifties and hundreds.

It now opens with the hook: how would you feed a crowd of five thousand?
Jesus sat them in groups of fifty and a hundred and the impossible became
shareable. We did the same with the unreached people groups, and each list
prints as a packet and a poster so one person can carry one.

THE NUMBERS ARE MEASURED, NOT ROUNDED TO THE BRIEF. The brief said about
thirty lists of fifty to a hundred; the catalogue says 27 lists covering
2,106 groups, with sizes from 8 to 118 and only 18 of the 27 actually
inside that band. So the copy says 27 and 2,106, and says ABOUT fifty to a
hundred rather than claiming every list is -- a reader who adds up the
rows would otherwise catch us out, and this is the same discipline the
funnel page already applies to K.

Eleven locales, agent-written per locale rather than translated from the
English, with the scripture reference in each locale's own convention
(Luke 9:14 / Lucas / Luc / Лк. / لوقا ٩:١٤ / 路加福音). The three-key split
with its emphasis span is unchanged, so the markup does not move.
2026-09-28 19:41:29 +00:00
Doxa Maps
c54a11d5aa The legend's Export path gets the busy state the HUD already had
Found by the bug-CLASS sweep, which is the point of doing one: the busy
indicator was added to the selection HUD after a report that pressing its
buttons showed no sign of work, and the legend's Export menu -- Poster and
Packet -- had none at all. Same defect, different surface, and I fixed the
reported instance without looking for its siblings.

Both do the same slow work: two dynamic imports, a map-SVG fetch, a funnel
fetch, then a multi-sheet render. onLegendExport now guards re-entry (a
second click would run two renders over one print frame), sets busyAction
so the control says what it is doing, and clears in finally on every exit
path including the early return inside the try.

The gate's busy assertion had two holes that let this through, and both
are fixed. It counted setters by matching the literal strings 'packet' and
'poster', so a ternary assignment was invisible -- a regex that enumerates
the shapes it knows about will always miss the next one, so it now matches
the assignment and excludes the clear. And it only asserted a whole-file
count, which "some handler somewhere sets it" satisfies; it now asserts
PER FUNCTION for all three print paths, so a surface cannot be forgotten
while the file as a whole looks fine.

Mutation-tested: removing the busy handling from onLegendExport now fails
naming that function, and dropping a finally fails naming its own.

Also drops a stale "page 3" from a shipped CSS comment -- page order is
variant-dependent since lightweight started omitting two sheets, so a
fixed page number in the output is wrong even where no reader sees it.
2026-09-28 19:41:29 +00:00
Doxa Maps
2c5fdbd7ac Rebuild the consumers main left stale (build owner, README rule 5)
Some checks are pending
Deploy to GitHub Pages / build (push) Waiting to run
Deploy to GitHub Pages / deploy (push) Blocked by required conditions
check:composite-freshness found stale on main: none. Rebuilt, tree + SRI
regenerated; the landing precondition (fresh, one folder, SRI) passed in main-capture scan.
2026-09-28 19:33:30 +00:00
Doxa Maps
40623dd3be One key for the set charts' loading line, in all eleven locales
Authored here and landed by studio, per the library boundary. The English is
byte-identical to the call-site fallback; each script takes its own ellipsis and
spacing convention rather than the English one.
2026-09-28 19:27:28 +00:00
Doxa Maps
042111bf3f Seat the legend caret in the title bar, and let the target keep a narrow screen
Some checks are pending
Deploy to GitHub Pages / build (push) Waiting to run
Deploy to GitHub Pages / deploy (push) Blocked by required conditions
Two things the Driver found, both about a control being in the reader's way
rather than about it not working.

The collapse caret was absolutely positioned over the panel's top-right, and the
title was hidden when collapsed — so collapsing left a lone glyph floating on the
map with nothing to say what it would reopen. It now sits inline in a header row
beside the title, which is where the shared legend puts its own: a flex item with
no positioning at all. The header stays visible when collapsed, so the control
always carries its label.

Side by side there is room for the target and its decoder. Stacked on a narrow
pane there is not: the decoder is a tall card that pushes the chart off the
screen, so a reader who asked to see the target got a glossary. Below the
breakpoint the legend now starts closed behind a named toggle. It is not hidden —
the control says what it opens, and opening is one tap.

The toggle reuses keys that already exist in every locale rather than adding a
word of its own. A new string would have meant eleven catalogue entries owned by
another tab and landing in another batch: a cross-owner dependency bought for one
label, when the fleet already had the sentence.
2026-09-28 19:13:38 +00:00
Doxa Maps
16ff7e719b Give the centered figure its own positioning context and a height floor
Some checks are pending
Deploy to GitHub Pages / build (push) Waiting to run
Deploy to GitHub Pages / deploy (push) Blocked by required conditions
The map inside is absolutely positioned to fill its figure, but the figure was
not a positioned ancestor, so it resolved against the panel instead and covered
the legend beside it. The bounded set's figure already carried position:relative;
this one did not — the difference two files drift into when the same rule is
written twice rather than shared.

In the narrow column a flex child sized by its content can resolve to zero
height, which is what a chart measured at 0x0 is. The figure now has a floor, and
stretches rather than centring itself into nothing.

The legend takes a flex basis instead of growing to its content. Reported
squeezed to 145x145 at 1440: a card that is only told a maximum will still take
what it asks for, and the figure had nothing but min-width:0 with which to argue.
2026-09-28 18:57:46 +00:00
Doxa Maps
e2086c4d2f Make the legend's full-screen button do something
The ring legend declared a fullscreen emit and rendered a labelled, focusable
button for it. The parent bound select and not fullscreen, so the button looked
right, tabbed right, announced itself right, and did nothing. It is a Driver ask,
and it is the half-wired shape exactly: the visible half worked, so nothing
appeared broken.

The fix is not to bind the emit. The button is now the shared control, so there
is one implementation instead of two — and it brings the part a second one would
have missed. iOS Safari does not implement requestFullscreen on arbitrary
elements and fails silently, so the shared control carries a pseudo-fullscreen
fallback. A hand-rolled handler would have worked everywhere I could test it and
nowhere I could not.

A first attempt kept the hand-rolled button and called the shared component's
toggle through a template ref. That component has no defineExpose, so under
script setup the method is private and the call would have resolved to undefined
— silently reintroducing the same defect while appearing to fix it. Checked
before committing rather than after.

The panel element is passed in, because full screen should show the chart and the
legend together, and the legend does not own the panel it sits in.

Found by sweeping for the half-wired-config class rather than by anyone reporting
it, which is what that class is for: nothing about it looks wrong from outside.
2026-09-28 18:57:46 +00:00
Doxa Maps
7e9d4d62cb Point the set charts at the promoted modules, and clear a committed conflict
The layout modules moved to library when they were promoted, and three of my
files still named the old paths: the two new whiteboard components, which were
written before the move landed, and the geometry check, which was written after
it and still copied the old import from its neighbours.

GiBoundedSet also carried an unresolved conflict — markers and all — committed by
a cherry-pick loop of mine that resolved generated output automatically and swept
the source conflict along with it. The file had both imports of the same symbol,
one of them pointing at a deleted path. It parsed, so the SFC check passed; only
the bundler would have caught it, and only at the point where it could not
resolve the file.

The lesson is about the loop, not the file: resolving one class of conflict
automatically is fine, but `git add -A` afterwards does not distinguish the
conflicts you meant to resolve from the ones you did not. A loop that stages
everything needs to refuse when a marker is still present.
2026-09-28 18:57:46 +00:00
Doxa Maps
16fd9c6207 Pin the whiteboard geometry, and record what the poison arm disproved
Three properties, each with the arm that catches its own defect: the bounded
set's gap survives projection, the drawing stays inside the Mercator clamp, and
a radius expressed in map units doubles with every zoom level.

The first arm taught me something I had assumed the opposite of. It narrowed the
gap to a tenth of a percent expecting the margins to go negative. They stayed
positive: the projection is conformal, so it preserves the ratio exactly, and a
narrow gap survives just as an eight percent one does. There is no distortion for
it to be eaten by.

So "the gap survives projection" is close to a tautology, and a check resting on
it alone would look like a guard while testing almost nothing. What can actually
fail is the ordering of the constants, and that is what the arm now poisons. The
narrowing case is kept as a passing assertion rather than deleted, because the
fact that it is safe is worth stating: the boundary cannot be eroded by the
projection, only by someone editing the numbers.

The clamp arm holds for every chart at once. The ceiling belongs to
extentFraction alone, not to a chart's extent — the scale is extentFraction over
extent and the outermost point sits at the extent, so the product is always the
fraction. Asserted across four extents, identical to three decimals.
2026-09-28 18:57:46 +00:00
Doxa Maps
092585af6c Put the bounded set on the whiteboard too, with its boundary in map units
Both set charts now pan and zoom. The chart still makes one claim — which side
of the line a group is on — so nothing here encodes distance or rank; what zoom
buys is reading the names, because at two thousand dots the inside is a cluster
until you can get close to it.

The boundary is the shared dish layer, which is the same layer a language
family's circle uses, so it is not a new type for either of us. Its radius takes
an exponential-base-2 zoom interpolation rather than a fixed pixel value.
circle-radius is screen pixels: a constant would hold its size while the dots
spread out from under it, leaving the circle correct at exactly one zoom and
wrong at every other — and for a dot near the edge the inside/outside reading
would silently invert, which is the only thing this chart says. Pixels per degree
double with each zoom level, measured at exactly 2.000000 between adjacent
levels, so the interpolation tracks map units exactly rather than approximately.

The boundary is drawn as a line with a transparent fill. A filled disc would say
the inside is a region, when what the chart means is a threshold.

The cross rides in the point collection as its own tier rather than as an
overlay, so it pans and zooms with the drawing instead of floating above it.
2026-09-28 18:57:46 +00:00
Doxa Maps
b68685801e Show the centered set as the whiteboard map rather than a static figure
The panel now mounts the zoomable chart in place of the SVG. Pan, zoom and
labels that appear by tier as the reader moves in were the ask; the engine
supplies tiling and label collision that an SVG version would have to
reimplement, and badly.

The printed component stays SVG, which is not an inconsistency: a sheet has no
zoom and no hover, so paper and screen genuinely want different renderers. What
they share is the ring vocabulary, so the two cannot drift apart in what they
call a ring even though they draw it differently.
2026-09-28 18:57:46 +00:00
Doxa Maps
1163d488b3 Draw the centered set on the whiteboard, so it pans, zooms and labels by tier
The ask changed and so did the call. A static target is fine in SVG; a zoomable
one carrying ~2,100 labels is not. The engine tiles the collection per zoom so it
draws less when zoomed out, and its symbol layers do label collision, which is
the part that would take longest to do badly by hand.

The label staging is one layer per tier with its own minzoom: band names when
zoomed out, ring names in the middle, group names in close. None of that is
tree-shaped, which is why it is a shared layer rather than a copy of one.

Coordinates go through the shared projection, never a scale. It is the exact
inverse of the renderer's transform, so a circle stays a circle; a linear scale
would draw concentric rings as ovals worsening outward. The only number this
chart owns is its extent.

Dimming a deselected ring is a paint expression rather than a filter, for the
reason the SVG version dims instead of hiding: a ring emptied of its dots reads
as a ring with no groups, which is the opposite of what a reader selecting it
just asked for.

The layer id is read back from the handle rather than written from memory. The
first version reached for a name the module does not use; inside a try/catch that
does not throw, so the dimming would simply never have happened — a legend that
appears to do nothing rather than an error. Asking the handle means a rename
cannot silently disable it.
2026-09-28 18:57:46 +00:00
Doxa Maps
8390f06465 Project the set-chart features onto the whiteboard, and report the real ceiling
The projection is passed in rather than built here, so the chart and its checks
share one instance and this module still owes nothing to a coordinate system it
does not define. The dish takes its radius from the projection's exposed scale:
deriving it by projecting two points and subtracting would be right on the
equator and wrong everywhere else.

The guard reports the outermost latitude, because that is the real limit and
shape is not. The projection is the exact inverse of the renderer's transform, so
a circle stays a circle at every extentFraction — measured at 0.000% aspect
error. An earlier reading of mine said otherwise and was wrong: it computed
screen x as longitude times cos(latitude), a plate-carree correction Web Mercator
never applies, which manufactured a distortion the renderer does not produce.

What does bite is the pole. The outermost point reaches 52 degrees at the default
0.17, 77 at 0.35, 83 at 0.45, and Web Mercator is undefined at the poles with
engines clamping near 85.

That ceiling is a property of extentFraction alone, not of a chart's extent:
the scale is extentFraction over extent, so the outermost point is at
extentFraction whatever the extent, and every chart reaches the same latitude.
Measured across extents 100, 228, 260 and 1000 — identical to three decimals. So
a consumer cannot buy headroom by choosing a bigger extent, and a check written
against one chart's ceiling holds for all of them.
2026-09-28 18:57:46 +00:00
Doxa Maps
ff9edd25f7 Shape both set charts as tiered points, and leave the projection to the caller
The shared whiteboard layer draws a point collection where each feature carries
an id, a label, a tier and a priority, and stages its label layers by tier and
zoom. This decides which features exist, what tier each belongs to and how they
rank — which is this chart's business — and it does not convert to degrees.

Ring and band labels are placed on the radii the chart already draws rather than
on a second calculation, so the label and the ring it names cannot drift apart.
Groups rank by population, so when two names collide the larger people wins: a
reader zoomed out should meet the names carrying the most people, not whichever
was drawn first. Bands outrank rings outrank groups, because that is the order a
reader zooming in meets them.

Coordinates out are abstract on purpose. Converting them is a projection, not a
scale — the whiteboard shrinks degree height outward so the map's own stretch
cancels it, and a linear scale skips the cancellation. Measured on a ring of 720
points: with a linear scale the rendered aspect is 0.9968 at the inner ring and
0.7952 at the outer, so the target draws as ovals that worsen outward, worst
exactly where the ring labels sit. With the projection it is 1.0000 at every
ring.

That measurement also settles where the guard belongs: at the inner ring the
linear error is three parts in a thousand and would pass any sane tolerance. An
assertion placed there alone would be inert against the one mistake it exists to
catch. It goes at the outer ring, and at both.
2026-09-28 18:57:45 +00:00
Doxa Maps
eb0ef2b3a0 Rebuild wall-of-people-groups, and regenerate the tree
Some checks are pending
Deploy to GitHub Pages / build (push) Waiting to run
Deploy to GitHub Pages / deploy (push) Blocked by required conditions
2026-09-28 18:53:16 +00:00
Doxa Maps
fcad0dc1a0 Wall of People Groups: comment times and the unnamed-group fallback in the reader's language 2026-09-28 18:52:57 +00:00
Doxa Maps
8dcadc16d1 The country list never received the configured research URL
Some checks are pending
Deploy to GitHub Pages / build (push) Waiting to run
Deploy to GitHub Pages / deploy (push) Blocked by required conditions
Found by the bug-class sweep. RegionCountryMap is passed :research-url; the
UupgsList beside it is not — zero research mentions in its entire tag. So a host
setting profile-config.researchUrl got it honoured on the map popup's Full
Profile button and silently ignored by every profile link in the country list,
which fell back to a hardcoded doxa.life default.

The shape is worth more than the line: one config, two consumers, only one
bound, and the unbound one carrying a plausible default. A missing binding that
fell back to an empty string would have rendered a visibly broken link; this one
renders a working link to somebody else's site, which is why it survived. It is
invisible in either file alone — the parent's tag list has to be read against
the children's props.

One line. Bundle rebuilt; the binding is in the artifact.
2026-09-28 18:49:16 +00:00
Doxa Maps
a45f1914a6 Let the legend collapse on desktop, the way it already does on mobile
Some checks are pending
Deploy to GitHub Pages / build (push) Waiting to run
Deploy to GitHub Pages / deploy (push) Blocked by required conditions
Mobile has had a fold control since the narrow layout existed; desktop had none,
so a reader who wanted to see the map underneath had no way to move the legend
off it. The state was already there — every section read "not narrow OR open", so
on desktop the OR was always satisfied and the flag governed nothing. Now the
flag alone decides at both widths.

The control is deliberately not a new one. Same 22x20 caret, same rotation, same
aria contract as the shared SemanticTreeLegend's collapse button, which the
mobile legend also copies. Matching it costs nothing and means a reader who has
met one of these legends has met this one.

Collapsed, the panel shrinks to the caret. Hiding the contents alone would have
left a 380px box of empty background over the map: the reader presses collapse
and gets a blank panel instead of their map, which is worse than having no
control at all.

The choice is remembered for the session, so it survives working with the map but
not a fresh visit, and a remembered choice beats the width default — otherwise
dragging a window across the breakpoint would silently reopen a legend that had
been deliberately closed, undoing a decision rather than responding to one.
Storage access is wrapped: a sandboxed embed throws on it, and a legend that
cannot remember is much better than a map that will not load.
2026-09-28 18:45:13 +00:00
Doxa Maps
20355ca552 Rebuild country-posters, and regenerate the tree
Some checks are pending
Deploy to GitHub Pages / build (push) Waiting to run
Deploy to GitHub Pages / deploy (push) Blocked by required conditions
2026-09-28 18:40:44 +00:00
Doxa Maps
a1be5c7db3 Country Posters: a partner's basemap, night look, search switch and start view reach the map inside it 2026-09-28 18:40:21 +00:00
Doxa Maps
c312fc14b8 Rebuild upg-neighbor all-people-groups wall-of-people-groups prayer-walk-planner, and regenerate the tree
Some checks are pending
Deploy to GitHub Pages / build (push) Waiting to run
Deploy to GitHub Pages / deploy (push) Blocked by required conditions
2026-09-28 18:35:31 +00:00
Doxa Maps
786a834610 Map errors in every language, and print buttons that show they are busy (UPG Neighbor, All People Groups, Prayer Walk Planner, Wall) 2026-09-28 18:34:16 +00:00
Doxa Maps
01b02e3781 Rebuild the consumers main left stale (build owner, README rule 5)
Some checks are pending
Deploy to GitHub Pages / build (push) Waiting to run
Deploy to GitHub Pages / deploy (push) Blocked by required conditions
check:composite-freshness found stale on main: key-cities
template-bundle; also rebuilt: all-people-groups big-picture cities-internet country-posters dashboard-plus-grid demo-map doxa-maps-spa doxa-research-map doxa-simple-map doxa-vision-card golden-path-check gospel-internet gospel-resources my-upg-100-list people-groups-grid plugin-studio pplr-data-maps prayer-walk-planner regions-dashboard research-dashboard research-map-plus-grid stack-demo upg-neighbor wall-of-people-groups. Rebuilt, tree + SRI
regenerated; the landing precondition (fresh, one folder, SRI) passed in main-capture scan.
2026-09-28 18:22:47 +00:00
Doxa Maps
07e086ff23 check:legend-top-offset runs in gates:pre; a grid comment states its rule instead of quoting it 2026-09-28 17:54:33 +00:00
Doxa Maps
f7f180df50 The legend held a slot for a toolbar this profile never draws
DRV1650-SR-LEGEND-FILLS-SEARCH-GAP. On Simple Research's Regions and Languages tabs the
search bar is gone but the legend does not rise into the space. The other tabs are fine.

The two-tab split was a predicate, not a coincidence. With no geocoder the legend rises
to 14px UNLESS showsFlyTools() is true, and that returns true for exactly 'doxa-regions',
'language-family' and 'language-families'. It stops at 56px to avoid a MEASURED collision
on country-posters, where LegendTools renders at top:14px — the legend's own slot.

★ BUT showsFlyTools() DESCRIBES THE LEGEND TYPE, NOT WHETHER THE TOOLBAR EXISTS. Fly and
Clusters are leaf-only: the composite — which is what a reader calls "Simple Research" —
passes legendTools:false, so the toolbar is never drawn there and the 56px reserved a
slot for nothing. rega measured it at 1440x900 on main e1d0bc20: the band from the map's
top edge (y 44) to the legend card (y 100) is 56px of EMPTY map on both tabs, with no
LegendTools anywhere in the strip.

So the offset now asks whether the toolbar is RENDERED, not whether the type allows one.
Keying it on the tab name would have closed the Driver's gap and reintroduced the
country-posters collision the moment the tools were shown — the guard is kept, it is
simply asked the right question.

showLegendTools moved ABOVE legendTopVar, which reads it. A computed body is lazy so a
later const would have resolved at runtime, but this file has a TDZ history and
check:setup-order exists because of it; position is cheaper to keep honest than to reason
about.

check-legend-top-offset asserts BOTH inputs are consulted — drop either and it fails —
and that the declaration still precedes its reader. Negative-tested.

This is reading (b) of the two I put to dx-shepherd, chosen because rega measured it
rather than because it was likelier. Reading (a) would have been a cosmetic change to a
toolbar that turned out not to be there.
2026-09-28 17:53:39 +00:00
Doxa Maps
5880e84616 Cards per row on the shared grid, with Auto as the absence of a value
The Driver asked for a top-right dropdown with Auto plus fixed overrides,
remembered, and said plainly that the CSS already working must not break.

So Auto writes NOTHING. The grid is repeat(auto-fill, minmax(240px, 1fr)),
which already gives one column on a phone and three to five on a desktop by
measuring its container; Auto leaves the stylesheet alone and only an explicit
override sets grid-template-columns. An Auto that computed a number and wrote
it back would look identical until the container resized, and then be silently
wrong — the same picture, with the responsiveness removed.

An override is honoured even when it is a poor idea: five columns on a 390px
phone is a 62px card. That is deliberate. The reader asked for it, Auto is the
default and one click away, and the control shows which is active. Clamping
someone's explicit choice to what we think is sensible is how a control stops
being trusted.

It is remembered per PROFILE, not per bundle. Two grids on one page are two
different lists, and useDashboardLayout already paid for that lesson when a
shared key made one caller inherit another's saved layout. Blocked storage is
caught and ignored: in private mode the default is the correct outcome, not an
error to report.

It sits in its own row rather than in #filters, which is gated on
useSearch || useFilters — a host turning both off would otherwise lose a control
that has nothing to do with either. It modifies the grid, so it lives on the
grid.

Both strings are authored in all eleven catalogues, in the snake_case this
catalogue uses: a camelCase key here would have resolved to nothing and rendered
its own name, which is the defect I shipped in a legend earlier today.
2026-09-28 17:53:39 +00:00
Doxa Maps
594a3bbe53 Make the two Mapbox arms ask for Mapbox, and fail when they measure nothing
These land with the change that would otherwise hollow them out. Both gates
reached the Mapbox engine by filling a token into the published snippet, which
worked only while a token selected the engine. It no longer does, and the
snippet now pins provider explicitly, so both Mapbox arms would have run
MapLibre and still passed.

Both now name the engine. The shelf gate REPLACES the provider key rather than
prepending a second one: a duplicate key parses to whichever came last, which is
a coin-flip that happens to work today.

The more useful half is the vacuity check, because the old assertions were all
of the form "no request used the wrong token" — trivially satisfied by making no
requests at all. Each Mapbox arm now fails if it contacted Mapbox zero times.
In the shelf gate the two flags are twins set from one expression, so an arm
cannot be exempt from both: under maplibre zero requests are required, under
mapbox at least one.

Without that pair, the day a Mapbox arm silently runs MapLibre both arms pass
and the gate measures one engine twice.

I cannot run either: both are Playwright and the browser belongs to rega. Syntax
checked, the browser-gate guard still passes across 405 files, and gates:pre is
green — but the behaviour these assert is unverified by me and needs rega.
2026-09-28 17:53:38 +00:00
Doxa Maps
79a566a594 Publish the provider in every embed snippet, so the opt-in is discoverable
The snippet is the only thing a partner ever reads, and before this none of the
36 mentioned provider at all. So when a token stopped selecting Mapbox, the
hosts affected were exactly the ones who had pasted a real token into this
snippet — and they had no way to see the lever that would give it back.

Emitting "provider":"maplibre" restates the new default, which looks redundant
until you ask what the snippet is for. Stating the default is what makes the
alternative visible. A one-line HTML comment above the element names the opt-in.

Generated, so the change is in the generator; the 36 files follow from a tree
run. Verified 36 of 36 carry both the key and the note.

⚠ THIS CHANGES WHAT TWO BROWSER GATES MEASURE, and they are not mine to edit.
check-map-embed-cells and check-experimental-shelf both fill YOUR_MAPBOX_TOKEN
into this snippet to exercise the Mapbox path. With provider pinned to maplibre
they will get MapLibre and make no api.mapbox.com requests at all — so an
assertion like "every api.mapbox.com request carries the host's token" stops
failing because it stops having anything to check. Each needs to swap the
provider to mapbox in the snippet it builds, the way the shelf gate already does
for its leaf URL with ?provider=. Flagged rather than silently left to surface
as a green run that measured nothing.
2026-09-28 17:53:38 +00:00