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. |
||
|---|---|---|
| .. | ||
| api | ||
| colors | ||
| components | ||
| composables | ||
| constants | ||
| data | ||
| i18n | ||
| layouts | ||
| maps | ||
| scripts | ||
| stores | ||
| styles | ||
| utils | ||
| index.js | ||
| ProfileLoader.vue | ||
| README.md | ||
library/ (@map) — the shared map template
The other half of the architecture (see ../plugin-designs/README.md):
every map in plugin-designs/<your-map>/ composes the reusable, parameterized pieces that live here.
Fix something here and every map benefits on the next build; break something here and every map
breaks — read components/WARNING.md before touching shared pieces.
| Folder | What lives there |
|---|---|
ProfileLoader.vue |
The root of every bundle: parses profile-config, loads the profile, provides the stores + config |
components/ |
Shared UI — map, legends, controls, popup (PeopleGroupDetail), tables, charts, hints (README) |
composables/ |
Shared logic — map engine (useMapInstance), provider switch, shadow-DOM styles, theming |
stores/ |
Per-instance Pinia stores (ui / map / data) — provided by ProfileLoader, injected by children |
api/ |
Data-source registry (sources.json) + fetch layer — the JAMstack runtime wiring |
colors/ |
Color sets maps opt into via profile-config.colorSet |
i18n/ |
The shared string catalogs (all maps speak the same languages) |
data/ |
Small shared datasets baked at build time (big served data lives in ../data/) |
constants/ |
Shared constants |
scripts/ |
Maintainer pipelines (posters, research) — not part of any bundle |
The rule of thumb (same as WARNING.md): custom code in your app-profile, reusable code here — and things only move from the first to the second once they've earned it.