Commit graph

30 commits

Author SHA1 Message Date
doxa-maps-88655c70
3c17245c73 packets: give CI something to check, instead of a sweep that always SKIPs
Some checks are pending
build-gate / build (push) Waiting to run
check:packets-all is in gates:post. CI runs it on every build. It has SKIPped
every single time, because library/scripts/posters/out/ is gitignored — both the
records cache and every generated packet — so a clean checkout has none.

Every packet assertion in this tree has therefore never once been evaluated
there: the 12 prayer points, the wheel's 12 labels, page 1's geography paths,
the print layer, the build stamp. Several ticks of work accumulating on a gate
that had only ever graded files in my own working tree.

A SKIP that is always taken is indistinguishable from an unwired gate — and the
wave-membership census cannot tell them apart, because membership measures
invocation, not evaluation. That census is how check:packets-all was created in
the first place, which makes this the same class recurring one level up.

I raised this at the end of the last tick and deferred it on the premise that
closing it "adds a live-API call to every build". The premise was wrong.
packet.mjs already had an OFFLINE mode, and a fixture needs no network at all.

The fixture is the smallest zone — Tibet, 43 slugs, 20KB — with image_url
stripped so CI can never fetch 43 photos from someone else's CDN. It is DERIVED,
not hand-maintained: membership comes from the committed clusters.json, so
check:packet-fixture regenerates and content-compares it. Re-adding an image_url
fails; dropping a record fails naming the slug.

A fixture packet stamps itself "fixture", and "fixture nomap" in CI, which has
no poster sources either. `fixture` implies nophotos in the gate — without that
it reported "every source record carries an image_url", a true sentence about
real records and a false one about this packet.

A real bug in my own change, caught only by running the script through its REAL
invocation path: packet.mjs fails closed on a missing VITE_API_BASE_URL BEFORE
loadRecords() ever looks at PACKET_RECORDS, so `bun run packet:ci` exited 1 on a
tree that had everything it needed. Every hand-test had passed because I was
supplying a dummy host. The guard now exempts fixture builds and still fails
closed otherwise.

From a genuinely clean state — the whole out/ directory moved away — the sweep
now reports "1 packet(s) checked, 1 passed". i18n verified through that same
path: a partial block (1 string, 5/12 points, 3/12 rhythms) renders 12 points,
12/12 wheel labels, wheel 3 translated and 9 English, 0 blanks.

check:packet-fixture joins gates:post (34 -> 35), README quote re-synced in the
same commit. Wave 35/35, packets-all 11/11 here and 2/2 in the studio.
2026-09-24 14:33:36 +00:00
doxa-maps-88655c70
64fed43449 packet: stamp how a packet was built, into the packet
Some checks are pending
build-gate / build (push) Waiting to run
out/packets/ is gitignored, so check:packets-all grades whatever .html files
happen to be sitting there — an accidental set, not a current one.

On this machine that was 11 freshly generated packets. In
doxa-maps-plugin-studio, which shares main with this worktree, it was two
NOPHOTOS=1 layout probes of mine from earlier ticks, and the sweep was RED there
for anyone who ran the wave. The gate was honest about each file it checked; the
set it checked was accidental.

The underlying problem is that a fast layout-iteration build and a shippable one
were byte-indistinguishable. The packet carried lang and dir and nothing about
its own provenance, so the only way to tell them apart was for the person
running the gate to remember and pass PACKET_EXPECT_PHOTOS=0.

packet.mjs now stamps data-packet-build with the flags it generated under, and
the gate reads it. The GENERATOR writes it rather than the gate inferring it:
had the gate guessed "no photos present, so probably a probe", it would have
excused genuine photo-fetch failures. Tested explicitly — a packet stamped
"full" whose map svg is then stripped STILL FAILS. The stamp records intent and
cannot excuse a defect the build did not declare.

Deliberately not a timestamp. An mtime staleness rule was considered and
rejected on evidence from this same session: packet-strings.json was 303s newer
than the packets while byte-identical to HEAD, because a probe had restored it
from backup, so an mtime rule would have failed a correct tree — the exact
mistake fixed two commits ago in the library-index gate. Content comparison is
not available here (regenerating hits a live API and takes minutes), so the
build records its intent instead of the clock.

The studio sweep goes from "2 checked, 0 passed" to "1 checked, 1 passed", with
the packet self-declaring build="nophotos nomap". Env vars stay as an override
for pre-stamp artefacts, and the declared-deliberate note now names whichever
source actually declared it rather than always crediting the env var.

All 11 locales stamp "full". i18n re-verified with a partial block (1 string,
3/12 points, 6/12 rhythms): wheel 6 translated and 6 English, 12 point rows with
6 translated, 0 blanks. Wave 34/34, packets-all 11/11 here and 1/1 in the studio.
2026-09-24 14:21:45 +00:00
doxa-maps-88655c70
e130cb672a packet generator: say when page 1 has no map, instead of printing a tick
Some checks are pending
build-gate / build (push) Waiting to run
`✓ laos  95 groups  8 pages  0 photos` — for a packet whose first page is blank.
The page count stayed 8 because a blank sheet is still a sheet.

The map one-pager is lifted from out/posters/<zone>.html, which a separate
manual step writes. When it is absent packet.mjs deliberately still builds, and
that fallback is right — but it was silent. The empty-groups branch three lines
above already warns and skips; the map branch had the same shape and no warning.

A gate catching this is not the same as the tool reporting it. The gate added
last tick runs later, optionally, and only when someone invokes it. The
generator prints its tick at the moment of production, which is the one output
an operator is guaranteed to see.

Now it prints ⚠ with the reason inline and repeats it in an end-of-run summary
naming every affected zone and the fix — a per-line mark scrolls off in a
27-zone run, and this is the one fault that cannot be seen by counting pages.

Both silent branches are covered, distinctly: a missing poster reports "no
out/posters/laos.html"; a poster that exists but carries no <svg> — a truncated
generate run — reports "out/posters/laos.html has no <svg>". An operator can
tell "never generated" from "generation broke".

Also adds the `packets` script. Its absence is why the posters -> packets
dependency was never expressible anywhere: packet generation was only ever a
hand-typed node invocation, so nothing could state what it reads.

Measured in both repos. doxa-maps-plugin-studio has 0 generated posters against
wt-packet's 28 — out/ is gitignored in both, so posters are build artifacts and
this is a missing build step, not a repo defect. A fresh studio run now warns;
wt-packet prints a clean tick. The size difference corroborates: 0.4 MB with the
map, 0.1 MB without.

i18n re-verified end to end since this touches the generator: a partial locale
block (2 strings, 3/12 points, 4/12 rhythms) renders the wheel 4 translated and
8 English, 12 point rows with 4 translated, 0 blanks, clean tick. All 11 locales
regenerated with photos — packets-all 11/11, each 12/12 wheel labels, 12/12
points, 13 geography paths. Wave 34/34.
2026-09-24 13:53:57 +00:00
doxa-maps-88655c70
e8ad1d56e6 packet: a partial translation renders 12 prayer points, not 3
Some checks are pending
build-gate / build (push) Waiting to run
packet-render.js resolved the prayer points with `L.points || EN.points`.
That is a whole-block fallback when the key is ABSENT, and TRUNCATING when a
translator has done some of the 12 — a 3-element array is truthy. Measured at
the rendered layer with a deliberately partial locale block: a page headed
"12 Prayer Points" containing THREE, points 4..12 silently gone, including the
Mark 5:1-20 passage point 4 prints in full. Nothing blank, no error raised.

Invisible in all 11 shipping locales because every one of them is complete.
It would have appeared on the next language added — and "a new language is a
NEW BLOCK, never a code change" only holds if a PARTIAL block degrades per key,
since half-finished is the normal state of a language in progress.

Now merges per point and per field against English: English fixes the LENGTH
and supplies every missing field, so a point translated for `text` but not
`sub` keeps the English `sub` — and keeps its NUMBER, which the merge supplies.

The sheet-sum invariant could not see this: a 3-point page is still exactly one
sheet, so every number check-packet-render printed stayed correct while nine
points were missing. It now measures and asserts the count. Mutation-tested —
reverting the fix prints "prayer points : 2 / 12" and fails.

All 11 locales regenerated with photos and swept: check:packets-all 11/11, each
12/12 points, including RTL ar and Devanagari hi. Renderer, generator and gate
synced to doxa-maps-plugin-studio, which also lacked the .points-title and
.wheel-title test hooks the new assertion needs.

Also wires check:capability-exemplars into gates:post (33 -> 34 steps). It reads
public/doxa-maps/manifest.json, a build artifact, so ordering mattered: `tree`
writes the manifest and is &&-chained immediately before gates:post, and the
gate SKIPs rather than fails when the manifest is absent. Full wave: 34/34.
The quoted gates:post line in the README is re-synced in the same commit,
because check:quoted-scripts goes red on the drift this commit creates.
2026-09-24 12:20:38 +00:00
doxa-maps-88655c70
3a468c2bc3 The poster print preview rasterises too, and a deployment can ship sources without rasters
Some checks are pending
build-gate / build (push) Waiting to run
posters/*.webp is not shipped by default (42MB; bundling pushed the tree to 80MB,
git-packed to 57MiB, and the catalog rejected the push). The DOWNLOAD path has
handled that since d154a76 — PosterWall fetches posters/<id>.html and rasterises
it in the browser. The PRINT path did not: it handed an <img> a url it never
checked, so on a default drop the SAME poster downloaded fine and PRINTED as
"Poster not available yet." That is the bad first impression
IDEA-POSTER-EXPORT-404-ON-DROPPED-FOLDER-TRADEOFF called out by name.

Three things were missing; all three are here:

1. printPoster now climbs the same ladder as the download path — probe the webp,
   and only on a miss fetch the .html, rasterise, and verify PIXELS before opening
   the dialog (an SVG in an <img> is an isolated document: a lost stylesheet prints
   a black square with no error). Blob urls are held until unmount, not revoked at
   print time — the preview <img> is still reading from them.

2. publish-html.mjs wrote into public/assets/posters, which is a GENERATED MIRROR
   that every `bun run tree` rmSync's. Its output never survived, which is a large
   part of why the fallback still had no input months after it shipped. Retargeted
   at data/posters, the tracked source the mirror is made from.

3. bundlePosters is one boolean over a directory holding 40.9MB of rasters AND the
   28 sources, so it could only ever ship both or neither — and "neither" is what
   made both full-size paths 404. posterSources:true ships the sources without the
   rasters, the combination that was previously inexpressible. DEFAULT UNCHANGED:
   28 sources are 27.9MB raw (6.9MB packed, measured with pack-objects), real
   weight in a published tree that needs a deployment decision, not a silent one.

VERIFIED AT THE RENDERED LAYER, not by reading source. check:poster-print-fallback
clicks the real control with every posters/*.webp ABORTED at the network layer —
the default-drop condition forced rather than hoped for — and asserts the HUD
enters "generating the poster" and settles clean. Mutation-tested: removing the
fallback produces 4 failures including "no posters/*.html was fetched". The other
branch is verified too — with a webp present it is used directly, zero html
fetches, no rasterise — so the extra probe is paid only on a miss.

★ THE GATE WAS WRONG THREE TIMES FIRST, each time blaming the product for a
harness fault, which is why it now asserts the HUD instead of an internal:
  - it matched /print/i, but the control reads "UPG prayer poster" — the product
    names the ARTEFACT, not the action;
  - it patched Document.prototype in the PARENT realm while openPrintFrame writes
    into the iframe's own;
  - openPrintFrame calls doc.open() AFTER append, and open() RESETS the document,
    discarding a write patched at append time.
Through all three the .webp/.html request counters looked corroborating, because
the wall loads both by itself. The HUD is what a user sees, survives a refactor of
the print internals, and cannot silently miss.

The 28 poster sources are committed because the fallback has no input without
them. city-* posters are deliberately NOT covered: they fetch OSM street tiles at
generation time, so they are not reproducible offline and their rasters must stay.
2026-09-24 08:07:22 +00:00
doxa-maps-88655c70
4cb04480cc Localize tab labels for it/ro/zh, and prove the catalog is actually read
Some checks are pending
build-gate / build (push) Waiting to run
120 keys across three locales: legend.json (12 each), search.json kinds (10
each) and tabs.json (18 each, previously absent entirely). Each locale follows
its own established vocabulary rather than a parallel one — Popolo/Popor/民族,
matching the terms already used in that catalog's detail and legend strings.
zh stays Simplified here, as the rest of this tree is, even though the prayer
packet's zh block is Traditional: a reader sees 民族 next to 祷告进度, never
the two files side by side.

Ceilings drop 94 -> 54 per locale. regions.json is the only i18n debt left in
the app: 54 keys of full sentences, three locales.

WHY THE NEW GATE. check:i18n-parity compares catalogs TO EACH OTHER, so it
certifies a file no t() call ever reaches — perfect symmetry, zero consumers,
green forever. Grepping for t('tabs. found ZERO call sites and tabs.json was
nearly recorded as dead. It is not: the only consumer builds the key
dynamically (SemanticTreeLegend.vue:40, 'tabs.' + (tab.i18nKey || tab.id)),
which no literal-prefix grep can see. The reverse error appeared in the same
pass — regions. and options. matched only poster-regions.mjs and a JS options
parameter. Static reachability is unavailable in both directions, so
check:tabs-localized renders the labels and reads them.

It asserts DIFFERENCE from English, not mere presence: te() falls back to the
hardcoded English label, so "shows some text" passes on the exact failure
being tested. Mutation-tested three ways — missing catalog FAILs, an all-English
catalog FAILs (first draft PASSed it, taking a per-pair exemption seven times),
one legitimate collision still passes with a note.

Both i18n gates now run in gates:post; neither was wired into the build before.

Also fixed, found by the build rather than sought:
- check-i18n-parity hardcoded an absolute path, so the studio copy silently
  graded wt-packet's locales and reported PASS on a tree it never read. Now
  repo-relative. That exposed the studio's own locale tree as a strict subset,
  103 keys behind including buttons.json:share — the very orphan this gate's
  header says it was written to catch, reappeared unseen in a second repo.
  Synced forward: zero studio-only keys, zero text conflicts, lossless.
- index.json still declared field-access-visas with rows:1146, a leftover from
  the commit that removed that table as private. The generator already filters
  private tables; the committed artifact predated the filter. Removing the
  declaration also removes the row-count leak. Copying the file in would have
  reopened the leak.
- check:sfc-parse needs an installed node_modules and is the first pre-gate to
  fail without one, so a dependency-less worktree reads as a parse error.
  Documented, with the reason switching it to bun only appears to fix it.
2026-09-24 06:23:30 +00:00
doxa-maps-88655c70
61dcd9b28e Merge main into bundler-1/work (regenerate the built tree, re-hash)
Some checks are pending
build-gate / build (push) Waiting to run
The nine conflicts were all in generated artifacts under public/. Built
files are not hand-merged: every bundle whose source either side touched
was rebuilt, the tree regenerated against main's new generate-tree, and
the hashes re-baked. Source merged clean — main never touched
PosterWall.vue or en/regions.json, they only appeared in a two-dot diff
because this branch changed them.
2026-09-24 04:28:25 +00:00
doxa-maps-88655c70
ddde518a85 Regions: expand a region to its country posters, reusing the shared wall
Driver request: "a toggle or carrot added to the bottom of the region
section that says view country posters, which will let you view country
posters for each of the countries in the regions" — with View-on-map and
Print, "the same reusable code from upg 100 list". This is a third
sanctioned addition alongside the tab strip and the no-selection chooser.

Almost nothing was built. library/components/poster/PosterWall.vue was
already the shared, fully parameterised grid, and it already owned the
card layout, the "Show on map" header button, the PRINT OPTIONS footer
and the client-side raster. regions-list.vue supplies only data and a
toggle — the same division country-poster-wall.vue states in its own
header.

- A "View country posters" toggle at the bottom of every region section,
  carrying aria-expanded and aria-controls, using the same caret SVG the
  header and footer links already use, rotated when open.
- Opening a region renders that region's countries. Measured: 41 cards
  for Africa and all 41 previews LOAD (naturalWidth>0 — a 404 still
  yields an <img>, so the load is the real check). No assets were
  copied: public/assets/previews already holds 174 country-*.webp and
  generate-tree already stages them.
- "Show Chad on the map" reaches the Region Dashboards tab with title
  "Chad" and hash "#chad", by mapping PosterWall's CustomEvent listId
  back through the country-selected the tabs already exchange.
- India prints its own reason with the exact number from the data; China
  renders a real poster. Only India is unsupported — 855 groups and no
  sheet, against China's 154 on a 3x4.

Two defects the screenshot caught and no assertion did:

- PosterWall's root is position:absolute;inset:0, because in its own
  bundles it IS a pane. Embedded in a region card it painted 1440x1100
  over the region's country list. Fixed by parameterising the shared
  component rather than overriding its CSS locally: new `embedded` and
  `sectionHeads` props, both defaulting to today's behaviour, so
  country-posters and key-cities are unchanged by construction. Both
  re-checked. Re-measured: root static, 1200px wide, panel below the
  grid, no overlap.
- With one unnamed group the wall printed a section head reading the
  literal "Other" plus a count duplicating the region card's own
  summary. Suppressed by sectionHeads=false.

The dataset moved to library/data/country-posters.json now that it has a
second consumer, one import line changed in country-posters, and
emit-countries.mjs was retargeted so the next generator run cannot
recreate the duplicate.

The three new strings do not exist on DX-life, so there was nothing to
copy and nothing was invented in another language: en only, with the
other seven locales falling back to the key.

Regression: 15 suites green, including region-vs-live 73/73,
country-vs-live 28/28, mobile 44/44 and 26/26, and a new
_probe-region-posters.mjs. check:library-index drifted for real this
time (3 new library files) and is regenerated here.
2026-09-24 04:26:29 +00:00
doxa-maps-88655c70
ec974b4c63 fix(posters): ask the legend which zones are reachable, not clusters.json
Some checks are pending
build-gate / build (push) Waiting to run
publish-html.mjs enumerated the raw cluster keys under a comment that said "the
zone ids the app can actually reach" — which was exactly what it did not do. It
then warned that deaf-old-world and deaf-new-world-asia-pacific were MISSING on
every run, when nothing was missing.

Those two are SUPERSEDED by the unified `deaf` row (affinity bloc A017), not
absent. Verified against live records instead of trusting the comment that says
so: 63 + 60 = 123 = A017 exactly, zero groups on either side of the difference.
They stay in clusters.json deliberately — three isolation-proof scripts assert
deaf_lists.length === 2 — but the legend does not expose them, so no user can
select them and a poster for them is unreachable by construction.

It now derives zone ids from leafZones(buildLegendNodes(...)), the same source
the app itself uses, so the two can never disagree again. Result: 28 posters
published, zero warnings, matching the legend's 28 leaves.

records:null, not records:[] — buildLegendNodes treats an empty array as truthy
and then demands population fields that are not available here.

A recurring warning that is always wrong is worse than no warning: it trains
everyone to ignore the one run where something really is missing.
2026-09-24 04:07:17 +00:00
doxa-maps-88655c70
467b7f01a2 feat(packet): all 11 locales — Hindi unblocked with a vendored Devanagari face
Some checks are pending
build-gate / build (push) Waiting to run
The packet now ships in every locale the app ships: en es fr pt de it ro ru ar
hi zh. Each carries all 21 translatable keys, the twelve prayer points, the
eleven rhythms, both attributions and the privacy note. point4_passage stays
omitted everywhere and falls back to the licensed English WEB text, for the
reason already recorded — translating scripture here would put unlicensed,
unverified Bible text into a printed prayer packet.

HINDI WAS BLOCKED ON A FONT, not on a translation. Devanagari renders as tofu
boxes in the packet's Georgia/Palatino/serif stack because this machine has no
Devanagari font at all. Noto Serif Devanagari (SIL OFL) is now vendored —
devanagari subset only, 124 KB, licence and provenance beside it, matching how
poppins/ ships latin and latin-ext separately.

It is base64-INLINED into the packet, not linked. A packet is a standalone file
meant to print offline — the Node generator writes HTML with no server, and the
photos are already inlined for exactly this reason — so a URL would break
precisely where the packet is most used. Cost: ~165 KB on a 3.5 MB packet, and
only for that language. One rule prepends the face to the stack rather than
editing 22 hardcoded Georgia declarations, and the browser falls back PER
CHARACTER, so Latin scripture references in a Hindi packet still set in Georgia.
mr and ne map to the same face; a missing font warns loudly rather than silently
producing rectangles.

Chinese needed no font work — CJK already shapes in the system stack — only the
translation.

★ TWO MEASUREMENTS LIED ABOUT SCRIPT COVERAGE, recorded so nobody automates this
cheaply. Comparing shaped width against a missing font reported ar and zh BROKEN
and hi FINE — exactly backwards, because Devanagari tofu boxes happen to match
real glyph advance. An ink-column-profile heuristic then scored CORRECTLY
RENDERED Devanagari the same as tofu (3 vs 2 distinct heights), because its
baseline bar gives a uniform profile. Both were abandoned: a detector that blocks
working packets is worse than none.

Verified: all 11 languages render 8 sheets -> 8 letter portrait pages, map
painted, zero overflow. The Hindi prayer-points page was READ AS A PICTURE —
conjuncts and matras shaped correctly, no boxes, Latin references still Georgia.
2026-09-24 04:03:57 +00:00
doxa-maps-88655c70
dd14b5fe76 feat(packet): five more languages, and right-to-left support for Arabic
Some checks are pending
build-gate / build (push) Waiting to run
The packet now ships in 9 of the app's 11 locales: en es fr pt de it ro ru ar.
Each carries all 21 translatable keys — page furniture, the twelve prayer points
with sub-headings, the eleven rhythms, both Metacamp attributions, the privacy
note. point4_passage (Mark 5:1-20 in full) stays omitted in every non-en block
and falls back to the licensed English WEB text with its credit, for the reason
already recorded: translating scripture here would put unlicensed, unverified
Bible text into a printed prayer packet.

RTL IS NOW REAL, not assumed. The renderer emits dir on <html> from an RTL
locale set. Without it an Arabic packet renders readable glyphs LEFT-aligned —
which looks like a styling quirk rather than a bug, and no assertion noticed.

★ TWO BIDI FINDINGS, both from reading the render rather than the code:

1. Scripture references are LTR runs inside RTL paragraphs, and the bidi
   algorithm REORDERS a run that starts with a digit: "1 Peter 5:6-11" rendered
   as "Peter 5:6-11 1". Each reference is now dir="ltr" + unicode-bidi:isolate.
2. With isolation, the reference LIST reads right-to-left on an RTL page (first
   item rightmost). That is correct for an RTL reader, not a defect — recorded
   in _meta so nobody "fixes" it. English source order is unchanged, asserted.

★ HINDI IS DELIBERATELY NOT ADDED. Rendering each script at 48px and READING THE
PIXELS: Latin, Cyrillic, Arabic and CJK all shape correctly in the packet's
Georgia/serif stack; DEVANAGARI RENDERS AS TOFU BOXES, because this box has zero
Devanagari fonts (fc-list). A hi block would ship a page of empty rectangles. It
is blocked on vendoring a Devanagari font into public/assets/fonts, which is its
own piece of work.

⚠ THE MEASUREMENT THAT LIED. My first test compared rendered text advance against
a missing-font baseline and reported ar and zh BROKEN and hi FINE — the exact
opposite of the truth, because Devanagari tofu boxes happen to match real glyph
advance. One screenshot settled in a glance what two rounds of clever
arithmetic got backwards. Recorded in _meta.

Verified at the rendered layer in all nine languages: 8 sheets -> 8 letter
portrait pages, 95 people cards, 12 wheel segments, map painted, zero
overflowing sheets, 19 strings differing from English per locale. The Arabic
prayer-points page was read as a picture, not just asserted on.
2026-09-24 03:50:35 +00:00
doxa-maps-88655c70
41847076a2 feat(packet): ship the prayer packet in Spanish, French and Portuguese
Some checks are pending
build-gate / build (push) Waiting to run
The app ships 11 locales; the packet shipped 1. A Spanish-speaking reader got a
Spanish interface and then an entirely English prayer packet. The per-key
fallback machinery was built and verified several ticks ago — the catalog was
simply never populated, so i18n was functionally unshipped.

es, fr and pt now carry all 21 translatable keys: the page furniture, the twelve
prayer points with their sub-headings, the eleven prayer rhythms, both Metacamp
attributions and the privacy note. Terminology follows the app's OWN existing
locales rather than inventing a parallel vocabulary — "grupo étnico" for people
group and "no evangelizado" for unengaged come straight from
library/i18n/locales/es.

★ point4_passage IS DELIBERATELY OMITTED FROM EVERY LANGUAGE BLOCK, and that is
the point, not an oversight. It is Mark 5:1-20 printed in full, and the file's
own guidance already required "a public-domain or properly-licensed version for
that language". Translating scripture myself would put unlicensed and
unverified Bible text into a printed prayer packet. Omitting the key makes it
fall back to the licensed English WEB text with its credit line intact —
honest, and visibly English, which invites a real translation rather than
hiding the gap. The note in _meta now says this outright so the next translator
does not "complete" the block.

Also recorded, because the render made it visible: scripture REFERENCES stay in
English in every language ("Matthew 27:27" on a French page), so a reader can
find the passage in their own Bible. That is a deliberate tradeoff and it does
look foreign next to fully translated prose — _meta now names it and says how to
add localised book names properly if a language wants them.

Verified at the rendered layer in all four languages: 8 sheets -> 8 letter
portrait pages, 95 people cards, 12 wheel segments, map painted, zero
overflowing sheets, and Mark 5 correctly falling back to English with its
credit. The French prayer-points page was read as a picture, not just asserted
on: all twelve points translated, Matt 27:27 intact at point 1, "un lieu sûr" at
point 8.
2026-09-24 03:37:05 +00:00
doxa-maps-88655c70
cfea0c8e70 fix(posters): measure the full-version header instead of guessing 460
Some checks are pending
build-gate / build (push) Waiting to run
Four zones had NO POSTER AT ALL. measureHeader() opened with
`if (!SPARSE) return HEADER_BASE` — a flat 460 — so the per-zone measurement its
own comments describe ("HEADER IS PER-ZONE, NOT A SHARED CONSTANT...
measureHeader() predicts the height from the zone's OWN content") only ever ran
in SPARSE mode. Outside it, any zone whose header was taller than 460 tripped
the fail-loud guard and rendered nothing:

  oceania-indonesia                   504   andean-caribbean  504
  asia-china-tibet-autonomous-region  481   deaf              619

Raising the constant was not the fix. HEADER is subtracted from the map height,
so 460 -> 620 would have stolen 160px of map from the 25 healthy sheets to
rescue 4 — the exact tradeoff the per-zone measurement exists to avoid, and the
one its own comment records (tuned to North India, it overflowed west-africa;
tuned to west-africa, it stole from everything else).

MEASURED the real header on six zones instead:

  HEAD_BOTTOM = (headY + 34) + 118      exact on all six
  headY       = 191 + 23 * tableLines   exact on all four probed

The 118 is the two vision columns — a constant 104, because that copy is fixed
and wraps identically on every sheet — plus the 14px trailing gap. ALL per-zone
variation is the facts table, which advances 23px per wrapped line. So this is
arithmetic on the same inputs the renderer wraps, not an estimate, and it needs
no calibration history like the SPARSE model's +24/+52/+66/-8/+6 above it.

★ THE ERROR WORTH RECORDING: my first attempt derived the constant by READING
the code — tracing headY's assignments to 150 + 23n - 12 — and was 53px short on
every zone, because the scripture block between the table and the vision columns
also advances the cursor and I had not traced it. Every zone then failed,
including ones that worked before. Deriving it from four measured renders got it
exact. Reading selectively is not measuring.

Result: 28 posters render, zero failures. And the map GREW on the healthy
sheets, because they no longer pay for the worst case — laos' map frame starts
at y=443 against the old flat 460, while deaf takes the 627 it genuinely needs.

Still missing after this: deaf-old-world and deaf-new-world-asia-pacific, which
never reach the render at all (silent skip, exit 0 — they are `deaf_predicate`
selectors that leafZones() does not return). Different cause, carded separately.
2026-09-24 03:31:01 +00:00
doxa-maps-88655c70
52b7a110d8 fix(posters): one bad zone no longer costs the other 28
Some checks are pending
build-gate / build (push) Waiting to run
generate.mjs threw on the first zone whose header overflowed, so a single bad
sheet ended the run: 16 posters written, the remaining 13 silently unbuilt, and
a non-obvious short count as the only symptom.

Failures are now collected, the run continues, and it exits 1 at the end with a
census naming every zone that has no poster. The guard stays loud — an
overflowing header is a real defect and must not be downgraded to a warning —
but it fails the RUN rather than truncating it.

That change immediately surfaced a FOURTH broken zone the abort had been
hiding. All four overflow the same way:

  oceania-indonesia                   504 vs HEADER=460   (+44px)
  andean-caribbean                    504 vs HEADER=460   (+44px)
  asia-china-tibet-autonomous-region  481 vs HEADER=460   (+21px)
  deaf                                619 vs HEADER=460  (+159px)

Root cause, found but NOT patched here: measureHeader() returns a fixed
HEADER_BASE when SPARSE is off instead of measuring the zone's own content — so
the per-zone measurement its own comments describe only runs in SPARSE mode.
That is a separate fix with its own layout risk; carded rather than bolted on.

Also adds publish-html.mjs, the missing half of the browser-rasterise fallback.
That fallback fetches posters/<id>.html and NOTHING published it: out/ is
gitignored and public/ had zero .html, so a zone with no image 404s today. The
script writes zone poster HTML into public/assets/posters/, skips city-* posters
(they need OSM tiles to regenerate, so their images must stay), and warns
loudly about zones with no HTML instead of quietly writing fewer files.

★ IT IS NOT RUN AS PART OF THIS COMMIT, and the reason is worth recording.
Measured with real git packs:

  webp   5.0 MB raw  ->  ~5.0 MB packed  (already compressed; git cannot pack it)
  HTML  21.4 MB raw  ->   5.78 MB packed (text, repetitive across posters)

Swapping zone webp for zone HTML is about +0.8 MB PACKED — a net LOSS, not the
42 MB win the headline implies. The raw-byte comparison that made it look like a
win (21.4 vs 5.0) and the gzip comparison that made it look better still (5.8 vs
5.0) both mislead; what git stores is the number that decides it. The real 22 MB
is 176 city-* posters, which cannot be replaced this way at all.

So the deletion this was leading to is deliberately NOT done: it would have made
the repo bigger while being reported as a saving.
2026-09-24 03:20:50 +00:00
doxa-maps-88655c70
4475581c1b Close the visa-data leak at its factory + harden the gate
Some checks are pending
build-gate / build (push) Waiting to run
The field-access-visas table (580 KB -- how workers enter closed
countries) was found PUBLIC in three copies and LIVE-SERVED at 200:
the key-cities bundle data dir, the research out/ dir, and a stale
staging tree inside the served catalog. Root cause: emit() enforced
PRIVATE_TABLES but the ROUTE bundle-copy loop did not -- a privacy
rule enforced at one of two writers is not a rule. The loop now skips
PRIVATE_TABLES; the redundant source copies are deleted (data-private/
keeps the one canonical file); and the hygiene gate flags the
filename and visa_ fields as HARD findings in every mode -- this data
class never rides the strict-tier softening.

All three public copies and the live URL were purged first and
verified 404 by anonymous fetch before this commit.
2026-09-24 03:03:53 +00:00
doxa-maps-88655c70
7bba1c0771 feat(upg-100): the prayer card deck prints as CARDS, not as paper
Some checks are pending
build-gate / build (push) Waiting to run
Driver: "the print dialog that opens up needs to show all the cards not in a
paper format but a CARD format, the aspect ratio that is used in the card
generator."

The deck set @page{size:letter portrait} and gave each card height:100%, so 95
cards printed as 95 mostly-empty 8.5x11 sheets — and with NO photo, because
image_url was never in the fetch. Measured: 95 of 95 groups DO have a portrait,
so the omission cost every card the one thing that makes it a prayer card.

Now the PAGE IS THE CARD. @page takes the stock's own inches, so one card is
exactly one page: 95 cards -> 95 pages at 180x252pt (2.5x3.5in). Print on
cardstock and cut, or send the PDF to a card printer. The stock is a user
choice, per the Driver's ask to "choose the layout of the print dialog to choose
the aspect ratio": poker 2.5x3.5in (default), postcard 4x6in, A6.

⚠ @page SIZE MUST BE EXPLICIT INCHES. Chromium SILENTLY falls back to letter for
a named size it does not know: `size: A6 portrait` yields 612x792pt with no
error of any kind, while `size: 2.5in 3.5in` yields exactly 180x252pt. The gate
therefore asserts the MEASURED MediaBox — a check that grepped the stylesheet
would pass a deck that prints as paper.

Card content follows doxa-life/promotional-material-generation, the Python data
pipeline feeding the InDesign template: per language it emits Description,
Religion, Area, Country, a QR to that group's page, and the URL. Ported to
JavaScript here so the deck generates in the map app on click. Description and
Area are NOT available from this API — measured 0 of 2107 records carry either —
so the card is built from what genuinely exists: portrait, name, country,
population, language, religion. An empty block reserved for data that never
arrives is worse than a card designed around the data there is.

QR is hand-rolled (library/packet/qr.js, ~200 lines): byte mode, level M,
smallest fitting version, mask 0. A symbol is pure computation, so it costs one
small file instead of a package and runs identically under Node and in a tab —
the standing constraint is "without adding a lot of dependencies on the
internet". A QR that does not scan looks exactly like one that does, so it is
verified by DECODING: an independent reverse-path decoder re-derives the data
placement, unmasks, de-interleaves and parses the header. 5/5 round-trip,
including version selection (v2 for a short URL, v3 for real ones).

I18N: the QR encodes the LOCALISED profile URL, so a Spanish card scans through
to the Spanish page — verified, pray.doxa.life/es/aheu. Strings fall back per
key; checked with a partial Spanish block ("Ora por ellos" rendering while
Language/Religion stayed English).

★ ONE BUG THE NUMERIC GATE PASSED, worth recording. The first deck rendered
"LAO", "thm", "EAN" — raw codes on a card a person is meant to pray from —
because normalizeRecords() flattens each { value, label } dict to its VALUE and
parks the label under <field>_label. A gate counting "4 rows rendered" passed
it: the rows were there, just unreadable. The gate now asserts the values are
HUMAN-READABLE, not merely present.

Verified by check:card-deck across all three stocks: 95 cards / 95 pages, 0
broken portraits, 95 QR codes with real symbols, 0 raw codes, 0 overflow.
2026-09-24 02:56:20 +00:00
doxa-maps-88655c70
301cc38fba feat(upg-100): print a prayer packet from the map legend
Some checks are pending
build-gate / build (push) Waiting to run
Driver, as a first-time user: "I select one and it gives me print options at
the top right of the legend... that's the only way that map would be usable."

The affordance was already built and switched OFF. The shared legend emits
'export' with 'poster' | 'packet' for the selected node, the profile wired
@export, and SHOW_EXPORT_BUTTON was held false with the note "until the export
packet is finished: the button currently produces a broken packet". That
precondition is now met, so the switch is on.

ONE RENDERER, TWO CALLERS. The pages move to library/packet/packet-render.js,
imported by BOTH the build-time generator and the browser export, so the printed
packet and the generated one cannot drift. The card asked for exactly this:
"reuse the existing print actions rather than growing a fourth implementation."
The extraction is behaviour-preserving — the generated packet is byte-identical
to before apart from leading whitespace (the code is now inside a factory), and
check:packet-render still passes 8/8.

★ THE PACKET IS GENERATED ON CLICK, NEVER SHIPPED. One packet is ~3.5 MB (photos
dominate) and 28 zones would add ~100 MB to a repo that ALREADY had a publish
blocked at a 74 MB asset tree. Generating client-side costs zero repository
bytes; the photos come from URLs the records already carry. This is the Driver's
instruction: "instead of fetching the poster from the public folder I want the
client side code to generate the poster real time when you click on the button
so it does not take a lot of mb storage." Bundle cost: +21 KB (494 -> 516 KB),
which includes all 12 prayer points and Mark 5:1-20 in full.

Printing goes through the SHARED openPrintFrame — a hidden same-page iframe, not
window.open(). A popup is blockable and drops the reader out of the map.

Two traps the browser path had to handle, both already known to this codebase:
  - pg_slugs BYPASSES the shared selector evaluator (it intentionally throws on
    that type, and 12 of the 27 lists use it). Same special-case the map and the
    poster wall apply, so all three agree about who is in a list.
  - evaluate() returns a Set, not an array.

VERIFIED IN A BROWSER, not by reading source (new gate check:packet-browser):
live records -> shared renderer -> page.pdf(), giving 8 sheets -> 8 pages at
612x792pt, 95 people cards, 12 wheel segments, Mark 5 in full, 2 metacamp.org
attributions, 0 page errors. I18n verified with a partial Spanish block: the
translated headings rendered in Spanish while Mark 5 and the attributions fell
back to English, per key, never blank.

Card BUILD-UPG100-MAP-LEGEND-LEADS-WITH-THE-UPG-100-LIST part (d). Parts (a) and
(b) shipped earlier. Part (c) — removing the People Group tab and retabbing the
legend — stays OPEN: that card is explicitly driver_gated on "what is the
legend's tree once Region/Zone/People Group are removed", and the current tabs
are wired to real hierarchy data.
2026-09-24 02:43:49 +00:00
doxa-maps-88655c70
fd2077625a feat(posters): capitals and major cities on the country maps
Some checks are pending
build-gate / build (push) Waiting to run
The Driver asked for map detail: "a typical map poster needs details on the
map that is not hard to see, but not so much that it becomes too complicated
— cities and capitals makes sense."

Natural Earth 1:10m, PUBLIC DOMAIN: no attribution to carry onto a printed
packet, vendored so a build is reproducible offline with no runtime
dependency. Trimmed at vendor time from 4.9 MB / 7,342 places to 170 KB /
2,573 (national + admin-1 capitals, plus cities >= 250k) because a poster
cannot legibly carry 7,342 dots and the packet is downloaded.

The layer is deliberately SUBORDINATE. It answers "where am I?"; the
people-group pins answer "who do I pray for?", and they keep the only
saturated colour on the sheet. Capitals are ringed dots (the cartographic
convention), cities plain dots — shape and value carry the rank so no extra
hue is spent. Reading order stays country > province > city > people group.

Three things had to be right, each found by measuring rather than by looking:

1. RANK BY RELEVANCE TO THE SUBJECT, not by global importance. Sorting by
   capital-rank then population is the obvious choice and is wrong: the
   neighbours are usually bigger. On the Laos sheet that sort spent the budget
   on 12 Vietnamese and 8 Thai cities and printed exactly ONE Laotian place
   while 12 more sat available — a poster of Laos labelled with other
   countries' cities. Subject countries now win the budget.

2. DERIVE THE SUBJECT FROM THE PINS, not from the zone. zone.adm0 exists only
   on generated city/country zones; the hand-authored lists carry a selector
   in SEVEN shapes (pg_slugs, country_code, country_codes, wagf_block,
   wagf_region, union, deaf_predicate) and the most common names no country at
   all. Reading zone.adm0 produced an empty set and the whole sort silently did
   nothing. The pins always know their own country.

3. AVOID THE PINS USING WHERE THEY ARE DRAWN. Two distinct errors here. Testing
   the label's ANCHOR is wrong for a right-aligned label, whose glyphs run
   leftward from it — so the box is tested, not the point. And the de-overlap
   relaxation MOVES badges after projection, so proj(pin.lng,pin.lat) is not
   where the badge lands; this block therefore runs AFTER layoutLabels and reads
   laid[].px/py. With the naive tests, 7 of 25 labels sat within 14px of a pin
   and Saravan landed 1.4px away while the check reported it clear. Now: 291
   labels across 16 posters, ZERO overlaps. A city that cannot fit clear of a
   pin is dropped — a missing city costs nothing, an obscured people group
   costs the poster its purpose.

⚠ ROADS WERE REQUESTED AND DELIBERATELY NOT SHIPPED. Natural Earth's 10m roads
layer was fetched and measured: ZERO features for LAO, MMR, KHM, VNM, THA, NPL,
BTN, TCD, NER, MLI, SOM, AFG, YEM, IDN, IND and CHN. Its coverage is essentially
USA/CAN/MEX. It would draw a dense network on a US map and nothing on every
country these packets are for — a broken-looking map exactly where it matters.
Real road coverage needs an OSM extract: a much larger dependency with an ODbL
attribution obligation, so it is carded rather than half-shipped. See
data/README.md.

Note: zone oceania-indonesia fails generation with a header-overflow error.
Verified pre-existing on a clean tree — unrelated to this change.
2026-09-24 02:21:03 +00:00
doxa-maps-88655c70
1c24759e21 feat(posters): UPG-100 prayer packet generator, letter portrait, i18n
Some checks are pending
build-gate / build (push) Waiting to run
The packet as specified: page 1 the map one-pager, pages 2..n the people
groups 20-per-page with photos, then the 12 prayer points, then the prayer
wheel. Every string is externalised so a new language is a new block in
packet-strings.json, not a code change; fallback is PER KEY, so a partial
translation renders what it has and leaves the rest in English rather than
blanking. Verified with a partial es block before removing the stub.

Three bugs worth recording, each of which rendered silently wrong:

1. zoom, NOT transform:scale(), for print. transform is paint-time, so the
   sheet still RESERVES its 1941px layout box and the paginator breaks it
   across two pages, inking only the top of the first: 8 sheets came out as
   16 PDF pages. Negative margins do not help — they collapse the outer
   space, not the fragmentation box. zoom is layout-time, so pagination sees
   the scaled box. Measured: transform 2 pages/sheet, transform+margins 2,
   zoom 1.

2. The map SVG carries almost no presentation attributes — generate.mjs
   styles it from the poster document's stylesheet. Lifting the <svg> alone
   left those rules behind, and SVG fill falls back to its initial value,
   which is BLACK: 670 elements painted solid black with no error, no broken
   image, nothing any existing assertion could see. Fixed by carrying the
   poster's stylesheet minus its document-level rules, so a palette change
   in generate.mjs still follows the packet automatically.

3. LANG is the shell's POSIX locale ("en_US.UTF-8"), not a BCP-47 tag, and
   throws in localeCompare. PACKET_LANG is the only source.

The verification lesson is the bigger one. The probe PRINTED "PDF pages: 16"
for several runs while reporting the packet verified, because the bad counter
never looked at that value — a number that is displayed but not asserted on
is decoration. Then the first fix for the black map asserted on the first
<rect>, the one element just repaired, and went green while 647 others were
still black. Both assertions now test the property rather than a proxy: one
sheet must be exactly one page, and the map as a whole must not be
predominantly black-filled. Both were confirmed to FAIL on the known-broken
input before being accepted.
2026-09-24 01:39:26 +00:00
doxa-maps-88655c70
839b0c453d A letter-portrait sheet, an honest credit, and a privacy line
Some checks are pending
build-gate / build (push) Waiting to run
FORMAT=letter. The same poster on paper a home printer takes. Width stays
1500 for the reason the 3x4 note already gives -- every width-derived
measurement (the 330 gutter, the name columns, the header budget) was
calibrated there and carries over untouched. Only height moves: 8.5x11 at
width 1500 is 1941. Layout, text and spacing are unchanged; the extra height
goes to the label columns, which is where 95 names wanted it.

PRINTS AS ONE PAGE -- and this fixes the SQUARE sheet too. The stage is sized
in CSS pixels while @page declares a physical size, so a browser mapped 1500px
to 15.6in and TILED it: measured, the 3x3 sheet printed as FOUR pages. It went
unnoticed because these posters were always screenshotted by shoot.mjs, never
printed from a browser. A scale computed at generate time (page width in px
over stage width) makes one sheet equal one page on every format. Verified via
page.pdf: letter 612x792pt x1 page, square 216x216pt x1 page (was 4).

ATTRIBUTION CORRECTED. The credit said "(c) Mapbox (c) OpenStreetMap". This
file's own header says NO Mapbox, NO token, NO external fetch at render time,
and the basemap is bundled GeoJSON -- so it credited a vendor contributing
nothing, which is the kind of line that makes a later reader add a token "to
comply". The real source is geoBoundaries (the adm0/adm1 files carry its
shapeGroup/shapeName fields), CC BY 4.0, which REQUIRES attribution. OSM is
named only when OSM tiles are actually fetched (BASEMAP=1).

PRIVACY LINE on the sheet. People-group coordinates arrive already generalised
from the publisher -- deliberately not precise locations. A printed sheet
travels past the person who made it, so it now carries that fact itself. Worded
as provenance, not as a claim about processing we do not perform. It gets its
own row: sharing the credit row was a measured collision (516-984px against a
stamp ending at 543), and it is a sentence to READ, not a provenance stamp.

Also: a backtick inside a CSS comment inside a JS template literal broke
generation outright -- the trap check:template-literal exists for.
2026-09-23 23:57:42 +00:00
doxa-maps-88655c70
3a7913df64 Record why the table generator keys by ISO code and never by name
Extending last tick's external-source check from legends to the baked tables
turned up a real discrepancy: gaps-and-opportunities.json has 2,106 rows against
a 2,107-row corpus, with five name mismatches. The table says "Jerba" and
"Tmagourt" where the live API now says "Amazigh of Djerba" and "Amazigh,
Matmata" — the same Tunisian peoples, relabelled upstream since the snapshot.

It is harmless, and the reason is the point. Nothing joins on the name: the
generator keys everything by iso639_3, and the consumer does the same at
resources-map.vue:194, whose comment already states the rule and its stake —
"Never a name match: language names collide and drift ... a fuzzy hit here would
put the wrong scripture in someone's hands."

The generator followed that rule in four places and never said why. So the
decision lived only in the consumer and in my head, and a future author tidying
this file toward a name join would find nothing to stop them. They would
silently drop three groups and orphan two rows: no error, no empty state, just
fewer people on a map about people nobody has reached. That is precisely what
check-label-joins.mjs was written to prevent.

Verified rather than asserted before writing it down: the join key is clean on
every row — heart_iso639_3 present and well-formed on all 2,106, zero blank,
zero malformed, 862 distinct codes; bridge_iso639_3 blank on 461, which is
expected because not every group has a bridge language. gospel-resources/
data-tables renders 50 rows by 21 cells with zero console errors and zero
responses >= 400. Both citations in the new comment were checked against the
files they name.

Comment only; no behaviour change. Five gates pass, including check:label-joins
itself and check:legend-source from last tick.
2026-09-23 11:06:11 +00:00
doxa-maps-88655c70
159acac940 Point the app-tables guard at pages that exist, and let it count rows it can see
verify:full runs two browser guards that `bun run verify` skips, and both were
failing. Neither was a product defect; both had rotted unnoticed precisely
because verify-all.mjs marks them `browser: true` and skips them unless --full is
passed, so the build chain has never run them.

verify-app-tables was broken four ways, each of which alone made it useless:

Its default port was 8917, which nothing in this repo has ever bound. Now 4173,
the port `bun run serve` binds, so it runs with the same server as every other
browser gate. APP_BASE still overrides.

It navigated to `index.html#key-city-posters` and two sibling fragments,
resolved against the PROFILES object in layouts/app-wrapper-material. That shell
is not what `/` serves — as of 2026-09-22 the root page mounts <plugin-studio>,
and the built root contains ZERO of those three keys. The list is now three real
profile paths taken from manifest.json. key-city-posters is dropped rather than
renamed to something that merely looks similar: no such profile ships.

Its "are we on our page" check matched the shell title "Doxa Maps". A leaf
titles itself "<profile> · <bundle>". My first replacement matched an absolute
script path and was equally wrong, because a leaf references its bundle
relatively (src="../gospel-resources.js") — measured, on a page that was fine.
It now matches the SRI integrity attribute, which this build guarantees by
construction and no other server emits.

Its row counter only counted rows ONE level inside a shadow host, with no seen
set. A row in the light DOM, or one shadow level deeper, counted zero. Now walks
every depth.

The last defect is the one worth reading twice. The app header printed near the
END of the loop body, after the row check, so every verdict appeared ABOVE the
app it described and the whole report was offset by one. That made a PASSING app
(gospel-resources/data-tables, 40 rows) look like the failure and the real
failure look like it had rendered rows. I chased the wrong app through three
rounds of instrumentation before reading the output in order. A report that is
correct but misordered costs a wrong diagnosis, not just time.

Read correctly, the guard now says: cities-internet/data-tables 40 rows and 1
table fetched; gospel-resources/data-tables 40 rows and 1 table fetched; and
gospel-resources/resources-list fetches NOTHING and renders nothing. That last
one is the already-carded gospel-resources data gap, waiting on the three Driver
decisions — the component is built and correctly showing an empty result for
data it was never given. So the guard is reporting one real, known, Driver-gated
absence, which is what it is for.

Everything else in it was already sound and is untouched: the recursive
shadow-root walk for row counting, fail-closed on zero fetches, and third-party
request detection, which the file rightly calls a persecution risk rather than a
performance note.
2026-09-23 09:19:20 +00:00
doxa-maps-88655c70
9dc4dda8cf consolidate docs into documentation/ and deployment shells into deployment/
Some checks are pending
build-gate / build (push) Waiting to run
Driver 2026-09-21: 'a folder called deployment which will store the capacitor
folder, wordpress folder and tauri folder', and 'a folder called documentation
to store the contributing folder so docs are in one place'.

docs/ + contributing/ + CONTRIBUTING.md -> documentation/ (three doc homes into
one). capacitor/ + wordpress/ + src-tauri/ -> deployment/.

The public/ -> dist/ rename the Driver asked about is NOT here: after seeing the
measured blast radius (855 tracked files, 54 source refs, and public/ is the
served web-root four consumers point at) the Driver withdrew it - 'keep the
public folder the same'.

Path references were rewritten mechanically (258 across 87 files), but three
classes needed hand fixes because a prefix regex cannot see them:
  - join(ROOT,'docs',...) in 10 internal scripts - a quoted SEGMENT, not a prefix
  - tauri frontendDist '../public' -> '../../public', since src-tauri is now one
    level deeper; check:tauri caught this exactly as the card predicted
  - the WordPress contract test's dirname(__DIR__,2) -> 3, same reason

Also reverted one over-reach: the rewrite renamed a FINDING-CATEGORY label
'docs' in public-hygiene-check.mjs, which is a rule name in its own header, not
a path.

Verified: full build ALL GUARDS PASS, SRI consistent; check:tauri, check:mobile
(cap sync + webDir OK), check:wp-embed, quoted-scripts, capability-citations,
capability-exemplars, library-index, public-hygiene, parse and drop all pass.
2026-09-21 23:23:23 +00:00
doxa-maps-88655c70
2d64e21fbf Complete app-profiles->plugin-designs sweep across comments/docs; build green; layouts kept at root (load-bearing) 2026-09-17 18:18:19 +00:00
doxa-maps-88655c70
1a3925c6a6 Rename app-profiles -> plugin-designs; build green under new naming (load-bearing paths updated, guards pass) 2026-09-17 18:18:18 +00:00
swift-river542
b7d91e9aca Public data ships without working notes; unused data files stay out of the drop-in
Some checks are pending
Deploy to GitHub Pages / build (push) Waiting to run
Deploy to GitHub Pages / deploy (push) Blocked by required conditions
- data/ tables and the people-groups file: meta blocks reduced to provenance (source, url,
  downloaded, rows, grain, attribution); the link-verification table keeps only state and detail
  per link; generators write the same shape
- dropin.exclude in the build config: people_groups.csv, open-doors.json, platform-reachability.json
  and tables/index.json are not shipped (nothing loads them); sync-dropin honours it
- public-hygiene --strict flags note-style meta keys in shipped data
- Rebuilt bundles and gallery
2026-09-16 20:16:08 -05:00
doxa-maps
3fee91a193 Release governance: field-access data is private, never shipped
Some checks are pending
Deploy to GitHub Pages / build (push) Waiting to run
Deploy to GitHub Pages / deploy (push) Blocked by required conditions
data-private/ is born: datasets whose publication could help close doors stay
out of the web-root and out of public repos, permanently. field-access-visas
moved there; the tables index no longer lists it; the key-cities Field Access
tab is removed from the public build; the table generator routes it (and any
future PRIVATE_TABLES entry) to data-private/ and excludes it from the index.
Public-good data (internet gaps, gospel-resource gaps, people-group pairings)
ships as before.
2026-09-16 04:34:33 +00:00
doxa-maps
994b8d7dcf Prepare repo to the vision: BLUF vision-cast README, library warning, SRI injection, term scrub
Some checks are pending
Deploy to GitHub Pages / build (push) Waiting to run
Deploy to GitHub Pages / deploy (push) Blocked by required conditions
- README rewritten as a BLUF-first vision cast for vibe coders (key-terms glossary,
  origin story, features, requirements, data caveat, developer's note).
- library/components/WARNING.md: prototype in your app-profile; only proven,
  parameterized components graduate into the shared library.
- generate-tree.mjs: inject Subresource Integrity (sha384) + crossorigin into the
  standalone page <script> tags and the copy-paste embed snippets (manifest = registry).
- Scrubbed identity-tied phrasing from comments/docs, keeping the technical rationale
  and the mission content.
2026-09-15 22:58:19 +00:00
doxa-maps
35ac1c5607 docs: simplify all folder READMEs (what's here + graphic + link); migrate complex reference to docs/architecture/{data-sources,i18n,components,app-wrappers,poster-generation}.md 2026-09-11 18:50:31 +00:00
doxa-maps
d5ba21fa76 doxa-maps — portable bundler for embeddable JAMstack maps
Standalone repo that builds embeddable JAMstack map bundles (web components),
composes them into a lazy-loaded SPA via layouts/, and optionally wraps a
Capacitor Android/iOS app — one build, choose your output/CDN. Diátaxis docs.

History reset to a single clean commit; no PII, IPs, or internal terms.
2026-09-10 01:53:07 +00:00