Commit graph

183 commits

Author SHA1 Message Date
Doxa Maps
0bf8b88ff0 Regenerate the library index: the three packet charts and the items that landed uncatalogued 2026-09-28 15:01:26 +00:00
Doxa Maps
04bb5b264c Regenerate the library index and the capability exemplars
Some checks are pending
Deploy to GitHub Pages / build (push) Waiting to run
Deploy to GitHub Pages / deploy (push) Blocked by required conditions
Both are derived files that had drifted: an import count for utils/html.js, and
regions-dashboard's exemplar is region-dashboards now, not regions-list.
2026-09-28 14:53:59 +00:00
Doxa Maps
43130aa40e CONTRIBUTING: source and build output as two commits in one push (not two pushes), with the two exemptions
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 13:05:56 +00:00
Doxa Maps
f2e2509167 documentation: the browser guard, and ask the repo rather than grepping
Follow-up to the doc landing in 49, which was written before the guard existed.

Adds the guard: every browser gate reaches Playwright through
internal/lib/playwright-guarded.mjs and refuses unless browser-slot.sh exported
DOXA_BROWSER_SLOT; an unslotted run exits 75 with the command to use, quoted
verbatim. And says the slot is a COUNTING SEMAPHORE, not a mutex, with the reason
from its own header -- a single lock queued 10-19 deep for hours because the real
constraint is memory, ~300-500MB per chrome, not exclusivity.

Replaces the grep advice with `bun run browser-gates`. The grep still works and
is fine for a quick look, but it is a heuristic over file text: my own count from
grepping internal/check-*.mjs said 87 browser gates; the repo's own detector says
97, because browser gates also live in plugin-designs/*/scripts/. Corrected. The
authoritative list cannot drift from what is enforced, because
check:browser-gates-guarded uses the same detector.

One line the guard's header earns and this doc had not stated: being able to
launch is not the same as it being yours to launch. The guard stops the accident;
it does not hand a newcomer the seat. Testing whether the guard refuses is itself
a browser launch.

Does NOT claim path-into-node_modules detection: studio has that in landing 50,
and it is not in this tree, so the doc does not describe it yet.
2026-09-28 09:07:30 +00:00
Doxa Maps
a02458a171 documentation: which gates need a browser, and what exit 75 means
238 checks; 148 run anywhere, 87 need a real browser, 3 shell out. A newcomer
cannot tell from the name -- check:packet-render sounds like a file check and
drives a browser; check:composites-mount sounds like it needs one and does not.
The test is one grep for playwright/chromium, and the doc says so.

The part worth writing down is exit 75. run-parallel.mjs:262 defines three
outcomes, not two: 75 (EX_TEMPFAIL) is NOT MEASURED -- the gate could not look --
and a gate that exits 0 while announcing SKIP is also NOT MEASURED. Only another
non-zero is a FAIL. Two traps follow: exit 0 alone does not mean a gate looked at
anything, and your own `timeout` produces 124, which says you stopped it, not
that the code failed.

Also states the rule the split exists for -- one machine, one browser seat, so
one person runs them -- and why it is not just logistics: a source read and a
render are different measurements. Four catalogue gates can pass while a page
still paints English; and of 16 cards parked as "needs a browser", 7 were about
markup, a path, a manifest field or a gate, and settled headlessly.

Every number measured on main d8baaefd2 rather than estimated, including
correcting gates:pre from 13 to 15 after re-running it.
2026-09-28 08:31:18 +00:00
Doxa Maps
c73d713b43 Delete useMapPacket.js: nothing imports it since both packets print through the shared HTML renderer
research-map and countries-map moved to the shared printer, leaving the
composable with no importer on main. jsPDF stays: useMapPoster.toPdf,
posterSizes and my-upg-100-list still reach it. The file map row goes and the
library index is regenerated.
2026-09-28 06:44:14 +00:00
Doxa Maps
6451a2b158 bun run desktop:build <plugin> packages a desktop plugin as an app; check:targets keeps non-web plugins out of the web build
A plugin's <profile>.meta.json targets (web, mobile, desktop, widget; absent = web)
now decide where it is built. internal/plugin-targets.mjs is the one reading.
desktop:build builds a desktop plugin into .desktop/<plugin>/ with a page that
names its engine, builds its backend as a Tauri sidecar for this machine, and runs
tauri build with a --config override; tauri.conf.json and public/ are untouched.
The shell starts the bundled rudder-engine (RUDDER_AUDIO=browser, RUDDER_HOME in the
app data folder) and stops it on exit. A plugin whose targets leave out web
(Rudder: desktop + widget) is no longer built for the web tree: run-parallel,
generate-tree and check-composite-freshness skip it, and its public/ folder is
removed; the dev server still gives the Studio its corner. check:targets
(gates:pre, with a self-test of five planted violations) validates the values,
widget corners and backend folders and fails if a non-web plugin reaches the built
tree. Measured: desktop:build rudder-voice on Linux produced Rudder_0.1.0_amd64.deb
(8.8 MB) with the app and the engine side by side; the app has not been launched.
2026-09-28 06:44:14 +00:00
Doxa Maps
7077c55502 Remove a kill switch the code does not have
Some checks are pending
Deploy to GitHub Pages / build (push) Waiting to run
Deploy to GitHub Pages / deploy (push) Blocked by required conditions
file-map.md documented `SOW_EXPORT_BUTTON=false` as a hidden global switch that "hides the
fully-wired Export menu across ALL bundles". SOW_ appears nowhere in this repo, so anyone setting
it would get silence and conclude the Export menu was broken. Docs describe the code as it is, so
the claim is gone.

The per-component prop it mentioned is real and untouched: LegendMobile.vue passes
:export-enabled. Only the global env override was fictional. MAX_LIVE went with the staging.js row
it lived in, and neither name appears in README.md or documentation/ now.

If a global export switch is wanted, that is a card, not a doc edit.
2026-09-28 03:40:55 +00:00
Doxa Maps
9b6bcaa0fa Doc audit: five stale claims a newcomer would have acted on
Audited README.md and all 35 documentation/*.md against main. Script names and markdown links
were already clean — 59 documented scripts all exist in package.json, 0 broken links. What was
wrong was file-map.md describing files that no longer exist, and three paths that had moved.

REMOVED, all archived in afb998f17 or earlier and still documented as current:
  internal/staging.js — the repo-root staging page; the plugin studio is the only entry now
  index.html — the same page's HTML
  library/composables/useDoxaSearch.js — deleted in ea0c47b27, and dropped from a prose list too
REPOINTED: the three colour strategies documented under plugin-designs/doxa-research-map/src/colors/
live in library/colors/. THIRD-PARTY.md's two vendored files live under data/vendor/.
embeddability.md's reference to the staging script now says it was archived and where the entry is.

WHAT I DID NOT "FIX", because it was not broken: ~20 relative shorthand paths in the architecture
docs (`composables/useMapData.js` inside a document about library/) read correctly in context, and
three paths name things that are not repo files at all — a runtime endpoint (public/api/maps/token.json),
a generated artifact (out/_records.json) and a placeholder (./bundle/bundle.js). Reporting those as
defects would have buried the five real ones in 23 false positives.

TWO MEASUREMENT TRAPS I fell into on the way, both corrected before reporting: my first env-var scan
grepped only internal/, plugin-designs/ and library/ and declared 13 documented env vars unread —
11 of them are read in internal/build-lock.sh, .env.example, deployment/cloud/Dockerfile and the Go
backend. And an earlier sweep of my own data files reported six "rega" hits that were all the word
"aggregates".
2026-09-28 03:40:55 +00:00
Doxa Maps
febeff4990 Public build follows the production branch, and rebuilds itself when production moves
Some checks are pending
Deploy to GitHub Pages / build (push) Waiting to run
Deploy to GitHub Pages / deploy (push) Blocked by required conditions
main is the shared sandbox the dev server follows; /cdn is built only from PRODUCTION_BRANCH (default
production), which moves when someone promotes: git push origin main:production. The container watches
that branch and rebuilds /cdn once it has held a new commit for CDN_AUTOBUILD_MINUTES (default 2; 0 = the
admin button only); a failed commit is not retried automatically. The build fetches the branch into its
own ref (never touching the dev server's FETCH_HEAD) and uses that commit's own exclusion list, so
loosening it takes a promotion too. DEPLOY.md documents the promote step.
2026-09-28 03:37:09 +00:00
Doxa Maps
2e92a05b30 A preset keeps its profile's defaults: instance props merge over the meta config instead of replacing it
A preset page baked only the instance's props, so every default in the profile's
.meta.json config that the instance did not repeat was lost (go-int: upg-neighbor's
five presets dropped pinHint:false). instanceHtml now takes the meta config first
and the props over it. check:preset-config-merge (gates:post) reads every built
preset page and fails on a lost default; the rule is written into
PRESET-PLUGIN-STANDARD.md. Affected before the fix: upg-neighbor only; no
production preset has profile defaults to lose.
2026-09-28 03:12:45 +00:00
Doxa Maps
b7333ef970 HOWTO: build the Rudder desktop app on macOS or Windows, and what is not proven yet 2026-09-28 02:38:49 +00:00
Doxa Maps
1bbfeae07c documentation/DEPLOY.md: the 4 ways dx-maps ships, linked from the README
Some checks are pending
Deploy to GitHub Pages / build (push) Waiting to run
Deploy to GitHub Pages / deploy (push) Blocked by required conditions
Laptop (bun run dev, zero config); a shared cloud dev server (Docker or Coolify, with a persistent /data);
public embeds from /cdn or any static host; the built folder inside a site's public/, as DX-life does.
Each with the command, what you get, and the detailed doc.
2026-09-28 02:27:12 +00:00
Doxa Maps
60b479d8e7 The register said "unestablished" after it was established
upg100-plugin caught it: my THIRD-PARTY.md row 17 stated their licences table
was EMPTY on the very commit where it held all 162. Our landings crossed and
neither of us could see the other's tree. Worse than they framed it -- my own
two files disagreed, since data/boundaries/ATTRIBUTION already carried the 162
and the three duty classes.

Row 17 now records what is true: 162 licences established, 32 duty-free, 65
attribution-only, 65 share-alike class (58 ODbL + 7 explicit ShareAlike), with
the ODbL "Derivative Database" question marked OPEN and legal rather than
measurable. The "unverified, someone should look" paragraph drops from four
rows to three, because world-adm0's provenance is answered.

★ AND THE STRUCTURAL FIX, WHICH MATTERS MORE THAN THE ROW. The counts are
asserted in two files, so that is the one place these records can silently
disagree. The register now names their PROVENANCE.json as the AUTHORITY and
says plainly that if the two disagree, it wins -- drift becomes resolvable
instead of invisible. Their phrase for the failure is the right one: a register
that still reads "unestablished" after the work is done is settled-by-omission
in reverse, the absence of an update reading as the absence of the work.
2026-09-28 02:03:22 +00:00
Doxa Maps
4244c9097a merge main before the licence landing
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 01:57:25 +00:00
Doxa Maps
9cd99cba23 merge main before the licence landing 2026-09-28 01:57:11 +00:00
Doxa Maps
2a692021c0 Boundary attribution travels with the boundaries
The India ADM1 boundary is Creative Commons Attribution (CC BY 2.5 IN,
sourced from DataMeet and the Election Commission of India), so the credit
has to reach whoever receives the data. It did not: 6.1 MB of boundary
GeoJSON shipped with no human-readable attribution, and a sidecar for one
file of eight.

data/boundaries/ATTRIBUTION now ships beside the data. Every licence value in
it is verbatim from the geoBoundaries API, queried per boundary -- CC BY 2.5
IN for India, Public Domain for China. No licence wording is ours.

★ IT TOOK TWO ALLOWLIST EDITS, and the first one silently did nothing. The
boundary staging is TWO-STAGE: a pair list gathers into the build pool, and a
separate path list decides what reaches the tree. I added the file to the pair
list, rebuilt, and it still did not ship. An entry in one stage of a two-stage
pipeline is not an entry.

WORLD-ADM0 IS CORRECTED, not closed. This register first guessed Natural
Earth, public domain. It is geoBoundaries gbOpen assembled per country: its
features carry geoBoundaries' own iso3/shapeGroup/shapeName, and the plugin's
data/adm0/PROVENANCE.json names the source and endpoint. That file also states
the real problem -- gbOpen passes each country's UPSTREAM licence through, so
one file carries up to 162 licences and up to 162 attribution duties -- and
its licences table is EMPTY. Which of the 162 are CC BY is unestablished.
Stated in ATTRIBUTION as an undischarged duty rather than papered over.

pplr's row is exp-shepherd's evidence: the PPLR ArcGIS org aggregating IMB,
Joshua Project, ROP and WCD identifiers, publishing no licence at all, and
WCD is a proprietary Brill database.
2026-09-28 01:55:03 +00:00
Doxa Maps
308e1e82ab The joining page opens with the promise the setup actually keeps
repo's zero-config .env.example is on main (8c529c031), verified before writing about it: every DX_* and
VITE_* line is present and empty, empty meaning the local default. That is the follow-up patch I held back
rather than documenting a file whose contents had not landed.

"Every line in it is optional" is a better opening than the list I had, because it IS the zero-config
promise in five words — a reader who copies the file and fills in nothing has a working setup, which is
exactly what the Driver asked the default to be.

Also states what happens if the Mapbox token is left empty: the Mapbox-backed maps do not draw, and the
keyless MapLibre provider is the alternative. My previous wording implied the token was required to get
anywhere, which is not true and is discouraging at exactly the wrong moment.
2026-09-28 01:46:52 +00:00
Doxa Maps
e8ddfc34a6 Vue and Tabulator ship their MIT notices, and a third-party register
Some checks are pending
Deploy to GitHub Pages / build (push) Waiting to run
Deploy to GitHub Pages / deploy (push) Blocked by required conditions
Both are MIT, which asks for the permission notice to be included with the
software. Tabulator's minified .js carried only "(c) Oliver Folkerd 2025" and
its .css carried nothing; Vue's banner names @license MIT but omits the
permission text. Each now has its upstream LICENSE vendored verbatim beside
it -- fetched, not authored, with the URL and sha256 recorded.

★ AND THE ALLOWLIST IS WHY IT WOULD NOT HAVE WORKED. generate-tree stages
vendor/ by an explicit PATH LIST, so the licences sat in data/vendor and never
reached the tree: staged count 0. Vendoring a notice the staging step drops is
not compliance, it is paperwork. Both LICENSE paths are now listed.

documentation/THIRD-PARTY.md is the register the sweep produced: every
non-original thing dx-maps ships, its source, its licence, whether the licence
or attribution actually travels, and the gap. Compiled by inspecting the
SHIPPED tree rather than the source lists, because the question is what a
recipient receives.

Four rows are left OPEN and marked as such. Two are unverified and need a look
(world-adm0's provenance, the pplr upstream). Two need a decision, not a fetch:
people-groups and the 304 derived tables carry data whose redistribution terms
are not established, 36 MB of it. Attaching a licence I have not confirmed
would be worse than the gap, so those go to the Driver. geoBoundaries is
closable by work rather than judgment -- CC BY needs attribution, and the
.meta.json sidecar pattern already exists here for one file of eight.
2026-09-28 01:30:47 +00:00
Doxa Maps
2a328fe68f Every vendored font ships its own licence
Two of the five shipped without one. bebas-neue/LICENSE was 24 lines and
poppins/LICENSE 29 -- our own notes files named LICENSE, pointing at
openfontlicense.org instead of carrying the text. The OFL requires the licence
to travel WITH the font, and these fonts go out in the repo we intend to give
away.

Both now carry their FAMILY'S OWN upstream OFL, fetched verbatim from
google/fonts, which is what brings the real copyright line with it -- Dharma
Type for Bebas Neue, the Poppins Project Authors for Poppins. No licence text
was authored here. Source URL and sha256 are recorded. figtree keeps its OFL
and loses only the six lines of ours that sat above it.

Our prose moved to documentation/vendored-fonts.md, OUTSIDE the served tree,
and that placement is the whole lesson. My first attempt put a NOTES.md beside
each LICENSE, which check:public-hygiene rejected as "documentation in a served
folder" -- three new hard findings, plus a fourth for an internal folder name I
had relocated with the text. The old bebas-neue LICENSE had said exactly this
in its own words, "prose does not belong in a public served folder"; I quoted
that line and then did it anyway. The licence travels with the font; the
explanation travels with the repo.

Whole shipped tree, measured before and after rather than sampled:
role nouns 6 -> 0, hygiene warnings 18 -> 14, findings 1 -> 1 (the pre-existing
assets/ contract one, not mine).
2026-09-28 01:30:47 +00:00
Doxa Maps
ae899010cf The advanced page: working when your agent lives on a server
Driver via dx-shepherd (ENDTOKEN-UPG-DRV1 item 2). Written entirely from repo's exact text
(END-REPO-CLOUDNAMES) rather than from the architecture doc, because the last page needed two rounds of
corrections and every one was something I would have guessed wrong.

It opens by sending most readers away: "Most people do not need this page." The default is a fresh clone
and `bun run dev`, and a doc that implies otherwise costs a newcomer their first afternoon.

TWO OPTIONS, with option 1 recommended as the Driver asked:
  1. Keep working locally and follow the remote — one line, DX_SYNC_REMOTE. You keep dictation, your own
     database, and a broken push only breaks your own screen.
  2. Run the shared container — everyone opens one URL, and the trade-offs are listed honestly rather than
     buried: no dictation (the engine falls ~4 s behind speech on 2 CPUs, so it ships off), nobody can edit
     on it, one broken push breaks it for everyone, it needs ~2.5 GB and 2 vCPUs, and its database is the
     shared team one.

★ THE CONDITION I GAVE ITS OWN CALLOUT is the one that bites silently: the sync only fast-forwards when you
are on the branch with no unsaved edits AND no local commits. Otherwise it stops pulling and says "main is
N commits ahead". That is protective, and the consequence is not obvious — leave local commits sitting
while an agent lands twenty changes and the eventual pull carries a day of divergence. So the page says
"push your own work, or you will drift" in as many words.

EVERY CLAIM VERIFIED IN THE SOURCE, not taken from the message: deployment/cloud/ exists with its
Dockerfile and README, that README really does carry the OIDC_* settings (12 mentions), entry.sh defaults
PULL_INTERVAL to 15 and RUDDER_ENGINE to 0 — which is exactly the "it falls behind so we switched it off"
repo described.
No internal names: the only hit was `dx-maps-dev`, which is the Docker IMAGE TAG in repo's own command, not
a hostname. Same substring trap as 'aggregates' matching 'rega'.
Cross-linked both ways with HOWTO-join-the-table.md.
2026-09-28 01:12:43 +00:00
Doxa Maps
9889416b8c Local is the default and needs no configuration — reverting the team-database wording
Driver 01:10Z via dx-shepherd: the DEFAULT is everything LOCAL, zero config. A fresh clone plus
`bun run dev` gives the Studio, dictation, hot reload and a local database, and never reaches a remote
database or Docker. The team database is OPT-IN.

My 674d5655 said the opposite — that most teams point every laptop at the shared one and the local one is
"the fallback". That wording came from repo (END-REPO-PBDOC) and was correct when they gave it; the
decision moved the same day. Inverted here: local is the default and the opt-in line is what needs
explaining, not the other way round.

⚠ AND THE TABLE ABOVE IT WOULD HAVE KEPT SAYING THE OLD THING. Its middle row read "the team — this is
usually the default", six lines from a section now saying the opposite. Fixed in the same pass: the laptop
row is marked as the default, the shared row as opt-in. Third time in this file that a line has survived an
edit around it and quietly stopped agreeing with the prose — the only way to catch it is to re-read what
you did NOT change.

Also states the zero-config claim explicitly where a newcomer meets it first ("That is the whole setup.
Nothing to configure, no server to reach, no Docker"), because "everything is local" and "you have nothing
to set up" are different promises and the Driver is making the second.

Cross-links CLOUD-DEVELOPMENT-ENVIRONMENT.md for people whose agent runs on a server, with "most people do
not need it" — that doc is next and its exact env names are with repo.
2026-09-28 00:59:32 +00:00
Doxa Maps
674d56559a The joining page's database section, now that the team default is the shared one
repo END-REPO-PBDOC: the Driver's decision went live and REVERSED their earlier instruction to me. When I
first wrote this section repo said to leave DX_PB_URL out entirely, because it sat behind the team sign-in
and failed from a laptop. It is now the TEAM DEFAULT, and the local database is the fallback.

So the section is rewritten rather than appended to: a joiner is now told to set
DX_PB_URL=https://<your team's dev server>/pb, that they sign in with the same team account and there is no
separate password, and that leaving it unset still gives them a private local one — which is a fine way to
start. Placeholder host in the public text, not ours.

⚠ THE OPENING LINE WAS QUIETLY WRONG AFTER THE CHANGE and I nearly left it. "There are three databases and
you only ever touch one" was true when the local one was the only option a joiner had; now they touch the
shared one OR a local one depending on one env line. The claim that actually matters — and is still exactly
true — is that they never touch the third. Now reads "You work against one of the first two — never the
third." A sentence can survive an edit around it and stop being true without looking any different.

Added the detail repo gave that a reader needs and would not guess: new collections start LOCKED until a
migration opens them, deliberately, so nothing is public by accident. That reinforces the schema-as-commits
point the section already makes rather than sitting beside it.

Names unchanged and re-verified: DX_PB_URL points at the PocketBase root (.../pb), DX_PB_PORT for the
port-in-use case. No internal hostnames anywhere in the file.
2026-09-27 23:49:38 +00:00
Doxa Maps
ed04175a5d Re-cut the joining page against the Driver's follow-up
repo af37aa52 changed two things I had written emphatically, and studio confirmed both independently
(END-REPO-NAMES2 / END-STU-DOCFIX).

- DX_SYNC_REMOTE now takes a remote NAME **or** a full repo URL. My callout said "the remote's NAME, not a
  URL" — a sentence whose whole point is now backwards, so it is rewritten rather than softened. A URL saves
  a `git remote add`, and a private repo uses the laptop's existing git credentials.
- DX_SYNC_INTERVAL default is 5 seconds, minimum 2. It was 15/5.
- Added the reassurance studio named: a token in an `https://user:token@…` URL is never written to the log
  or shown at /api/dev-sync. That is exactly the question someone pasting a private URL will have, and the
  answer is good — worth stating rather than leaving them to wonder.

⚠ AND I DID **NOT** TOUCH LINES 38 AND 167, which also say "about every 15 seconds". studio flagged that
those describe the SHARED SERVER's pull loop — a different thing from DX_SYNC_INTERVAL that happens to have
carried the same number. Changing them because the number matched would have introduced a fresh error while
fixing a real one; checked both after the edit and they are intact.

Every other name still matches repo's branch. repo has also warned that a shared team database may later
become the default, which would make the PocketBase section go stale; they will tell me before it does.
2026-09-27 23:35:56 +00:00
Doxa Maps
3de124209a The joining page's env setup, written from repo's own text
repo answered with the names as they will ship (END-REPO-ENVNAMES) and dx-shepherd caught a path I had
wrongly removed. Both corrected; the doc now states nothing I have not verified.

⚠ MY "THE PATH DOES NOT EXIST" CHECK WAS WRONG, AND WRONG IN AN INSTRUCTIVE WAY. I searched the LAST
COMMIT's file list for pb_migrations and found nothing, then reported the path as unbuilt. `git ls-tree -r
origin/main` shows backend/pocketbase/pb_migrations/.gitkeep — it has been there all along. A file list from
one commit answers "was this touched recently", never "does this exist"; I asked the cheap question and read
it as the expensive one. The real path is restored.

THREE CORRECTIONS FROM repo, each of which would have cost a reader time:
- DX_SYNC_REMOTE is a remote NAME (`origin`), not a URL. My draft said "<the repo URL>", which would have
  failed silently for every joiner who copied it.
- DX_PB_URL IS OUT ENTIRELY. repo was explicit: it points at the shared database, which sits behind the team
  sign-in and does not work from a laptop. Telling a newcomer to set it would have sent them to debug an
  auth wall on their first afternoon. What replaced it is the truth — `bun run dev` downloads and starts a
  local database itself, and there is nothing to configure.
- VITE_MAPBOX_TOKEN is the one credential they must ask for, and my draft never mentioned it. A joiner
  following the old page would have got a working repo and a blank map.

ALSO ADDED, because it is the behaviour that makes the feature safe to recommend: the auto-pull only runs
when you are on that branch, with no unsaved edits and no local commits. Otherwise it does not touch your
files, it says "main is N commits ahead … pull when ready" in the log. That is the difference between a
feature a newcomer trusts and one they turn off after it eats an afternoon's work.

Required set is now stated plainly: DX_SYNC_REMOTE=origin if you want teammates' work to arrive, plus the
Mapbox token. Everything else optional, with its default in a table.
No internal names, re-checked after the rewrite: the only product name left is PocketBase, which a joiner
sees in its own admin UI.
2026-09-27 23:31:54 +00:00
Doxa Maps
1a7724b3c1 merge main into packet/vertical 2026-09-27 23:31:05 +00:00
Doxa Maps
64b6c523ef A joining page for people who have never seen this repo
dx-shepherd ENDTOKEN-UPG-DOC, from the Driver's laptop-first decision. Written for someone staring at a
repo, a dev server and a database, unsure which one they are allowed to touch — so it answers that first
and explains second.

THE SHAPE, in the Driver's own three places: laptop = your workshop · main = the truth · the shared server
= the table, look but do not edit. Plus the rule the whole thing rests on, one plugin folder per person.

★ THE 95% NUMBER IS THE POINT OF THE PUSH SECTION, not decoration. A newcomer whose first push is rejected
assumes they broke something. The measured result — 95% of pushes rejected, 0 conflicts, 1.1 retries on
average — tells them the rejection IS the normal case and explains why it is cheap: you collide on who goes
first, never on the same lines, because you are never in the same file. That reframes a scary error into one
extra command, and it is the strongest argument for the one-folder rule.

⚠ TWO THINGS I CORRECTED RATHER THAN SHIPPED:
- I first cited `backend/pocketbase/pb_migrations` as the migrations path. It does NOT exist in this repo —
  it is from the architecture doc's PLAN. A path a reader cannot find is worse than no path, because they
  assume they cloned wrong. Now says "committed alongside the code" until it is real.
- "reloads every few seconds" is now "about every 15 seconds", which is what the source actually states.

NO INTERNAL NAMES, checked rather than intended: zero hits for the server hostnames, the source-control
product, the agent tabs, the dictation tool, the auth stack, "tenant" or the project's own code names.
Cross-linked to develop-a-plugin-and-see-it-live.md, which answers the different question of how to BUILD a
plugin — this page is about working alongside other people once you have one.

⚠ THE ENV SECTION IS PROVISIONAL. DX_SYNC_REMOTE / DX_SYNC_BRANCH / DX_PB_URL are taken from the
architecture doc; repo is building them now and I have asked for the exact names, which are required vs
optional, and what each points at. A doc with a wrong env name is worse than no doc — the reader blames
themselves — so this gets corrected the moment repo answers, before anyone links it.
2026-09-27 23:28:40 +00:00
Doxa Maps
8afea17c6b Capability is derived from what a plugin composes, and the preset-plugin standard is written down
generate-tree derives each profile's tier from its composes edges (composing
anything is capability 4; composing a capability-4 plugin is capability 5). An
explicit type is checked against the derivation and a contradiction fails the
build naming both. check:capability-derived asserts every profile is 1-5 and
agrees. documentation/PRESET-PLUGIN-STANDARD.md describes the one
*.instances.json file a preset plugin declares.
2026-09-27 23:14:15 +00:00
Doxa Maps
655bbdc561 A filtered table becomes a link anyone can open, and the link now carries the typed search
Card BUILD-SHAREABLE-FILTERED-TABLES, for researchers and media-to-movements teams: narrow the table to
the peoples you are working on, then give that exact view to a teammate or reopen it on a phone.

TWO GAPS, both measured on main before building.

1. IT WAS NOT REUSABLE. The share hash was a PRIVATE function inside
   plugin-designs/research-dashboard/src/composables/useDashboardShell.js, so exactly one dashboard in
   the fleet could produce a link. go-int's board had none — grepped, zero matches — and every future
   kit dashboard would have had none either. Nothing in it was research-specific except its address.
   It is now library/composables/useShareableView.js, parameterized: the bus and labelFor are passed
   in, the filter and search param names are options, and no dashboard is named inside it. The
   research dashboard supplies the one genuinely research thing — labelFor, for the display-only slug.

2. THE LINK WAS INCOMPLETE, AND SILENTLY SO. The hash serialized filterSet ONLY. Typing in the search
   box is the commonest way this persona narrows a table, so a shared link reopened with MORE ROWS than
   the sender saw, with nothing to tell the recipient their set was wider. For population data passed
   between teams that is worse than a link that plainly fails. `q` is appended only when a term is
   present, so a dashboard with an empty search box writes byte-identically the hash it wrote before.

`&` joins params now, so it is reserved in values alongside `;` `:` `*` `,` `%` — otherwise a search for
"rice & beans" would truncate the link at the ampersand.

check:share-policy grows a DRIVEN arm: the two pure functions are lifted from the kit source and RUN,
so nine guarantees are measured rather than asserted — the old format byte-for-byte on an empty search,
slug still display-only, multi-value still dropping the slug, the typed search reaching the link and
coming back, a literal comma in a people-group name surviving, an ampersand in the search surviving, and
both legacy shapes (JSON hashes, and old #f= links with no q) still restoring. No browser needed to
prove a string round-trips.

Source only. Rebuild: research-dashboard, dashboard-plus-grid.

check:filter-multivalue arm C follows the file: it lifted escapeValue/unescapeValue from the shell to
drive the hash round-trip, and said so plainly when they moved ("the hash format moved and this gate is
blind to it") rather than passing vacuously. Repointed to the kit module.
2026-09-27 18:25:01 +00:00
Doxa Maps
6e5c0874d1 README shots: retake the 3 stale shots on b37138d7
Some checks are pending
Deploy to GitHub Pages / build (push) Waiting to run
Deploy to GitHub Pages / deploy (push) Blocked by required conditions
cap5-plugin-studio (Rudder's button now reads its label), prod-gospel-resources and its phone
shot. Derived from check:shots on this tree, tokenless; check:shots passes.
2026-09-27 09:58:51 +00:00
Doxa Maps
17763cdccc README screenshots retaken tokenless on 7a7ca67b (20 of 23; the Plugin Studio picture is kept until Rudder's corner button stops showing its raw key 'rudder-voice')
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-27 09:38:44 +00:00
Doxa Maps
68c7a6d4b6 Kit 4d-2: useDashboardData moves to the library
82 lines, one importer (useDashboardShell), and every import already @map/ — so it moves as-is with
no adapter and no shim. Its inject('profileConfig') stays inside its try/catch: it can be called from
outside setup, and that is the property the move had to preserve.

Source only. research-dashboard and dashboard-plus-grid need a rebuild.
2026-09-27 06:04:09 +00:00
Doxa Maps
6cc5542b70 One place decides how a count is punctuated, and it still says en-US
The numerals policy is HELD — whether counts follow the reader's locale is the Driver's decision — so
this changes nothing anyone can see. Every caller renders exactly the digits and separators it
rendered before. What changes is how many places that decision is written down: thirteen call sites
across four bundles becomes one, library/utils/formatCount.js, and eight of the thirteen (mine) now
read it.

That is the whole value while the policy is held. If the answer becomes "follow the locale" it is a
one-line edit in formatCount.js instead of thirteen edits by four owners, each free to pick a
different locale source — and the first divergence in that world would be invisible: two panels in
one dashboard, same number, different separators.

★ IT FORMATS. IT DOES NOT DECIDE WHAT EMPTY MEANS, and that is the bug this fold would otherwise have
shipped. I told dx-shepherd these three helpers were "byte-identical". They are not:
  DashboardDataTable     null/undefined -> ''   and '' falls through to Number('') === 0, so "0"
  DashboardDetailDrawer  null/undefined/'' -> '—'
  useDashboardShell      null/undefined -> '—'  and '' likewise becomes "0"
Three different answers to "what does a missing count look like", inside one dashboard. Folding them
onto one empty policy would have silently changed two of the three — a table cell going from blank to
an em dash, or an em dash where a zero used to be. So each caller KEEPS its own guard and hands
formatCount a value it has already established is real. The locale decision folds; the empty policy
does not, and the difference is written into the helper's header so the next person does not re-merge
them.

Folded: DashboardDataTable, DashboardDetailDrawer, useDashboardShell (its exported fmt keeps working
for the KPI strip), DashboardFacetRail's three inline template calls, people-groups-grid's two.
NOT folded, because they are other owners': regions-dashboard (3) and pplr-data-maps (2). The helper
is theirs to import when they want it; nothing forces them, and no gate demands it — a gate requiring
the helper everywhere would fail on their files today, which is not a thing to land on someone else.

Source only. research-dashboard, dashboard-plus-grid and people-groups-grid need a rebuild.
2026-09-27 06:04:08 +00:00
Doxa Maps
79e373d7cb Kit landing 4c-4: the breadcrumb completes the facet-cascade group, and three dead shims go
THE GROUP WAS SCHEDULED AS ONE LANDING AND DELIVERED AS TWO. Section 8 of the spec puts the facet
rail, the chip bar and the BREADCRUMB in a single landing. The rail moved in 4c-3 and the chips in
4c-2; the breadcrumb was left behind, and nothing failed, because it is mounted by nobody. A leftover
like that is invisible unless someone re-reads the spec against the tree, which is what this landing
came out of.

The breadcrumb gets exactly the inversion the chip bar got: no useTreeLegendLabels import, the
resolver read from dashboardConfig.legend, and the SAME unconfigured default — identity, joining an
array. Identical on purpose rather than by copy-paste: the breadcrumb and the chips render the same
filterSet in the same band, so if their defaults differed a half-configured dashboard would show one
value resolved and the other raw, side by side. Its dead `dataStore` / `searchSourceId` injects go
too; they existed only to build the composable that is no longer imported.

★ AND THREE ONE-CYCLE SHIMS FROM LANDINGS 2 AND 3 WERE NEVER RETIRED. plugin-designs/research-
dashboard/src/composables/ still held useDashboardLayout.js (11 lines), useSelectionCamera.js (11) and
useCsvExport.js (14), each a pure re-export of the library module. MEASURED before deleting: ZERO
importers. Every consumer — including DashboardMapPanel, DashboardDataTable, DashboardDetailDrawer,
useDashboardShell, gospel-internet's board and research-dashboard.vue — already reads
'@map/composables/…' directly, and a tree-wide grep for a relative import of any of the three returns
nothing. So they were dead code that made the plugin look like it still owned three library modules.

I wrote "a one-cycle shim that nobody retires is permanent" into the previous landing's gate comment
while three older ones were sitting in the tree unnoticed. They were not found by a gate; they were
found by listing what is still in plugin-designs and comparing it against the spec.

check:legend-mirrors arm 5 now guards three moved names instead of two, so a re-export of the
breadcrumb creeping back into the plugin barrel is caught the same way.

Source only. research-dashboard and dashboard-plus-grid need a rebuild.
2026-09-27 06:04:08 +00:00
Doxa Maps
a37ea4ffaf Merge branch 'main' of https://git.nuxtbase.app/doxa-life/doxa-maps into bundler-3/work
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-27 05:50:42 +00:00
Doxa Maps
7b6d02a568 Merge branch 'main' of https://git.nuxtbase.app/doxa-life/doxa-maps into bundler-3/work
# Conflicts:
#	package.json
#	public/dx-maps/doxa-maps-spa/manifest.json
#	public/dx-maps/gospel-resources/charts/index.html
#	public/dx-maps/gospel-resources/data-tables/index.html
#	public/dx-maps/gospel-resources/gospel-dashboard/index.html
#	public/dx-maps/gospel-resources/gospel-resources.js
#	public/dx-maps/gospel-resources/resources-list/index.html
#	public/dx-maps/gospel-resources/resources-map/index.html
#	public/dx-maps/manifest.json
2026-09-27 05:46:57 +00:00
Doxa Maps
eaafea0617 README shots: retake the three Big Picture shots on 34e3ef8a
Some checks are pending
Deploy to GitHub Pages / build (push) Waiting to run
Deploy to GitHub Pages / deploy (push) Blocked by required conditions
Their Religion and Region charts were empty (NEUTRAL, fixed at 34e3ef8a); check:shots passes.
2026-09-27 05:44:40 +00:00
Doxa Maps
9caf30e81a README shots: full tokenless retake on ca6979e4
All 25 that check:shots listed as stale, Arabic included; check:shots passes.
2026-09-27 05:43:40 +00:00
Doxa Maps
d4bd75f4ba Library index: a data file is described by its own _description or _meta, not its first line
generate-library-index took the first line of every file as its description, which for a .json file is data:
the Data section read '"_meta": {', '"value": "AFG",' and a run of ISO codes (flagged by simpleresearch).
A .json file now uses _description, then _meta.description, then _meta.purpose, and otherwise says
'(no description)'. iso3-to-alpha2 now reads as what it is.
2026-09-27 05:05:04 +00:00
Doxa Maps
63bd15713b Merge branch 'main' of https://git.nuxtbase.app/doxa-life/doxa-maps into bundler-3/work
# Conflicts:
#	public/dx-maps/doxa-maps-spa/manifest.json
#	public/dx-maps/gospel-resources/charts/index.html
#	public/dx-maps/gospel-resources/data-tables/index.html
#	public/dx-maps/gospel-resources/gospel-dashboard/index.html
#	public/dx-maps/gospel-resources/gospel-resources.js
#	public/dx-maps/gospel-resources/resources-list/index.html
#	public/dx-maps/gospel-resources/resources-map/index.html
#	public/dx-maps/manifest.json
2026-09-27 04:37:06 +00:00
Doxa Maps
873d73f5eb Merge branch 'main' of https://git.nuxtbase.app/doxa-life/doxa-maps into bundler-3/work
# Conflicts:
#	public/dx-maps/doxa-maps-spa/manifest.json
#	public/dx-maps/gospel-resources/charts/index.html
#	public/dx-maps/gospel-resources/data-tables/index.html
#	public/dx-maps/gospel-resources/gospel-dashboard/index.html
#	public/dx-maps/gospel-resources/gospel-resources.js
#	public/dx-maps/gospel-resources/resources-list/index.html
#	public/dx-maps/gospel-resources/resources-map/index.html
#	public/dx-maps/manifest.json
2026-09-27 04:17:14 +00:00
Doxa Maps
30c42b5f46 Library index: regenerated once for everything studio landing 10 adds to library/ 2026-09-27 04:14:32 +00:00
Doxa Maps
458ca5ff59 The prayer-progress cards become shared components too
The three goal cards, the stat row and their bars were the regions
dashboard's own markup. They are library components now, and the plugin
renders them instead of holding a copy:

  GoalCard      title + figure + a goal line pinned to the card's bottom,
                with a slot for whatever sits between
  StatCard      one figure and its caption
  ProgressBar   a single-value track, or a segmented one with a legend
  useCardSurface  the card surface both cards share, injected once per
                root under a stable label instead of copied into each

The bar and its legend now read from ONE list of segments, so a width and
the number printed beside it come from the same figure and cannot drift
apart; before, each was computed separately. A segment's colour is a prop
carrying the same token its .rd-seg--full/--partial/--none class used to
set, so a shared bar needs no vocabulary about prayer and those three
rules are deleted rather than left unreachable.

ProgressBar carries its own reduced-motion guard. The plugin's reset is
scoped to .rd-root and would not have followed the component anywhere
else, which would have reintroduced the one-second sliding bar their own
site suppresses.

The rendered DOM is unchanged for all four instances, measured by
server-rendering the real components and comparing against the markup
that was deleted, including that the bar widths equal the percentOfSubject()
they replace.

check:regions-uses-library grows to cover all four, asserting both halves
again: the plugin imports and renders them, and no longer ships their
markup or CSS. The .rd-root strong rule STAYS in the plugin -- UupgsList
renders a <strong> in the same shadow root and depends on it, which a
file-scoped read would have called dead.
2026-09-27 04:14:31 +00:00
Doxa Maps
725467a9de Country names in the reader's language, for all 242
Our data speaks ISO alpha-3. Intl.DisplayNames('region') takes alpha-2 or
UN M49 and NOT alpha-3, so every country name was stuck in English for want
of the join.

Vendored CLDR cldr-core 48.2.2 (Unicode-3.0, 36KB, sha256 and source URL
recorded in library/vendor/cldr/README.md). No npm dependency: a package
would pull the whole supplemental tree and make a shipped data file depend
on an install step, where a vendored copy lets the generator reproduce the
map offline -- which is the point of vendoring rather than merely having a
copy.

generate-iso3-to-alpha2.mjs joins it against the 242 codes in
countries.json and GRADES ITS OWN OUTPUT, writing nothing on failure: 242
coverage, injectivity, two-letter well-formedness, and that each documented
exception is still needed. KOS -> XK is the only exception -- Kosovo has no
ISO 3166-1 assignment, so no refresh will ever supply it, and XK is the
user-assigned code CLDR itself uses in locale ids. It is recorded with its
reason instead of patched into the vendored bytes.

countryName(iso3, locale) falls back localized name -> canonical English
name -> the code. Intl.DisplayNames THROWS on a missing or malformed code
rather than returning null, so the guard is checked before asking; the
probe demonstrates that throw rather than asserting it in a comment. An
unassigned-but-well-formed region returns the input unchanged, so that case
falls back instead of rendering "DE" as a name.

THE RESEARCH SPINE MAP IS NOT TOUCHED. iso3-to-iso2.json is a different
file with a different job -- the 176-country spine that drives the data
fetches -- and expanding it is a Driver decision, not a side effect of
adding country names. The generator cross-checks it and REPORTS
disagreements without rewriting. There are none: CLDR independently agrees
with all 176 values, including the two corrected this session.

Also fixes a duplicate "check:religion-label-not-value" key in
package.json, left by a merge. JSON keeps the last, so one description was
silently dead and check:doc-scripts could not see it; bun's parser warned.
The surviving entry absorbs the only fact the dead one carried, the two
library sites by name.
2026-09-27 04:14:31 +00:00
Doxa Maps
7e04a94c34 Kit landing 4c-3: the facet rail follows the chip bar into the library
The rail's rendering is unchanged. What changed is where its last two plugin imports point. 4b had
already removed the research COLOUR and LABEL tables -- those stay in plugin-designs and arrive on
dashboardConfig.legend -- which left exactly two, and both now read the module that OWNS the
function rather than a re-export:
  useDashboardAggregations -> @map/composables/useFacetAggregation.js   (extracted by 4c-1)
  useDashboardShell        -> @map/composables/useDashboardSearch.js    (extracted by 4a)
Pointing at the shim instead would have been invisible and wrong in a way this tree has already
measured: check:filter-multivalue carries the note from landing 3 that a gate left at the plugin path
read the shim, found no predicate to object to, and PASSED VACUOUSLY.

The dead injects go too. `dataStore` and `searchSourceId` were here only to construct
useTreeLegendColors and useTreeLegendLabels, and 4b removed both constructions. An unused inject is a
claim about the host: a kit component that appears to need a research dataStore is one a new
dashboard will dutifully provide before finding out it was never read. The chip bar shed the same
pair in 4c-2.

check:filter-multivalue and check:legend-mirrors follow the file rather than being left pointing at a
path that no longer exists -- the multivalue gate fails loudly on a missing panel, which is what it
is for, and it is how I would have caught forgetting.

UNLIKE THE CHIP BAR, THIS ONE IS ON SCREEN. 4c-2's component is mounted by nobody, so its gate was
the whole story. The rail is mounted by research-dashboard.vue and is 770 lines of tree, search and
drag behaviour: "the imports resolve" says nothing about whether it still opens four trees by
default or whether its search still narrows. It needs eyes.

The barrel re-exports from the library for one cycle, so no importer's line changes in the same
landing that moves the file. documentation/reference-library-index.md regenerated.
2026-09-27 04:14:14 +00:00
Doxa Maps
d54ece3b25 Kit landing 4c-2: the chip bar moves into the library, and its unconfigured default gets a voice
DashboardFilterChips.vue moves to library/components/dashboards/. Nothing about the rendering
changed. What made the move possible was 4b, which inverted the one dependency that blocked it: this
file imported useTreeLegendLabels — the People CODE -> NAME tables, research-specific and staying in
plugin-designs — and a library component importing a plugin composable is what
check:no-cross-plugin-imports refuses. The resolver now arrives on dashboardConfig.legend.

THREE THINGS BEYOND THE MOVE, all found by assessing the file rather than by moving it:

1. THE UNCONFIGURED DEFAULT NOW JOINS A LIST. 4b's fallback returned the value untouched, so a
   multi-value filter (spec D1) printed String(['A012','A013']) = "A012,A013", comma-jammed, while a
   dashboard that DID supply a resolver got "South Asian Peoples, Bengali Peoples". Configured and
   unconfigured should differ in whether CODES RESOLVE, never in punctuation — the second difference
   reads as a rendering bug rather than as missing configuration, and go-int is the consumer who will
   see the unconfigured state first.

2. THE DEAD INJECTS ARE GONE. `dataStore` and `searchSourceId` existed only to construct
   useTreeLegendLabels and have been unused since 4b. Removed rather than left in place: an unused
   inject is a claim about the host, and a kit component that appears to need a research dataStore is
   one a new dashboard will dutifully provide before discovering it is never read.

3. AN EXPLICIT EXCEPTION TO library/components/WARNING.md, recorded in the file's own header. That
   file says a component graduates into the library only once MORE THAN ONE map uses it; this one is
   used by ZERO, because the topbar dedup pass unmounted it when SearchFilterHeader became the single
   source of truth for showing and clearing filters. It is here because the kit's promise runs the
   other way round — a NEW dashboard mounts kit components, so waiting for a second consumer before
   providing the first one is a deadlock. The honest consequence is written down too: a passing gate
   means "it mounts and degrades", not "it looks right", and nobody has SEEN this component since the
   dedup. Whoever wires it in is its first real reader, and must not mount it beside a
   SearchFilterHeader that is already showing chips.

check:legend-mirrors grows arm 5, and it asserts the two things a "move" gets wrong that reading the
new file cannot show: that the OLD copy is actually gone (a move that leaves the original behind is a
copy, and two chip bars over one filterSet drift the moment one is edited — this tree has already
paid that with useNeighborT/usePplrT at Jaccard 0.82), and that the UNCONFIGURED behaviour is right,
which no dashboard here would reveal because they all supply a resolver. The default is LIFTED out of
the source and RUN, the way arm 3 drives the rail's fallbacks — not mounted, because a no-op renderer
cannot show a chip and the rendered result is rega's. Five mutants, all caught: old copy left behind,
the join reverted to 4b's bare value, a dead inject re-added, the supplied resolver ignored, and a
relative import restored.

The barrel re-exports from the library for one cycle, so no consumer's import line changes in the
same landing that moves the file. documentation/reference-library-index.md regenerated, per landing.

PARENTED ON a285e06c (4b), not on main, because 4b edits this same file — landing them the other way
round would revert 4b's inversion. Source only: research-dashboard and dashboard-plus-grid need a
rebuild.
2026-09-27 04:14:14 +00:00
Doxa Maps
25c102f5cd Kit 4c-1: the counting half, and the bucket that said "Other" in every locale
useDashboardAggregations has a split personality, and only half of it could ever
move. `lbl` and `aggregate` are pure counting — eleven lines, a Map, a sort, a
topN rollup, no facet name and no colour. Everything else in that file is
research: `colorFor` with its hardcoded religion / wagf_region / affinity_bloc
branches, the religion palettes, the donut and bar builders. Those stay, and
DashboardChartsRail keeps importing them.

The generic pair was what blocked kit landing 4c: the facet rail needs only that
half, and while it lived beside the palettes the rail could not follow it into the
library. Same split, same one-cycle re-export as 4a. Bodies verbatim.

★ AND THE ROLLUP BUCKET SAID "Other" IN EVERY LOCALE. aggregate() pushes
`{ key: 'Other', other: true }` for the tail, and that English word was rendered
directly by the facet rail and the charts rail. check:i18n-keys-exist could not
see it, because there was no T() call to inspect — the string was never a lookup,
it was a KEY being used as a LABEL. I found it while ASSESSING this landing rather
than while doing it, and it is fixed here rather than later for one reason: the
moment this module becomes kit code, an unfixed bug in it is exported to every
dashboard built from the kit.

THE TWO HALVES PULL IN OPPOSITE DIRECTIONS, which is the whole difficulty. The KEY
must stay English: the rail tests it to make the row inert, and the charts rail
refuses to filter on it, so a localized key would make the bucket CLICKABLE in
Arabic and filter on a value no row carries — an empty board, reading as a data
problem. The LABEL must not stay English. So OTHER_KEY is the sentinel,
otherLabel() reads dashboard.other, and they live next to the function that
produces the bucket so they cannot drift. Eleven locales, catalogue first.

This is the value/label trap for the third time — religion families, the language
tiers, and now a bucket in generic code. The third is the worst place for it,
which is why the rule now travels WITH the code that creates the value rather than
being remembered at each render site.

check:legend-mirrors grows arm 4, asserting both directions: translate the
sentinel and it fails; render the key instead of the label and it fails. Both
mutants run.

check-one-palette now scans the new module as well. Its subject — private palette
declarations — stayed in the plugin half, so nothing moved; adding the library
path means a palette smuggled into kit code later is caught by the same rule
instead of escaping into the library.

Library index regenerated in-commit. Source only. STUDIO: needs a rebuild of
research-dashboard and dashboard-plus-grid. The internal/ hunk is four lines in
check-one-palette.mjs.
2026-09-27 04:14:13 +00:00
Doxa Maps
de24e633f9 The region card becomes a shared component the plugin consumes
The Driver's ask, and his negotiating point with the DX-life author depends on it
being true: the components cloned from their /regions page must be REUSABLE,
PARAMETERIZED components in the shared library, not private to one plugin. He named
the failure mode himself — someone opens the regions plugin folder and finds the
components are not reusable after all. I checked before building, and for the UI he
named he was right: only the DATA and LOGIC had been promoted.

So library/components/regions/RegionSectionCard.vue takes title, summary, countries
and footerLabel, emits what was clicked, and knows nothing about where regions come
from. The plugin version fetched its own data and owned the layout, which is exactly
what made it unusable anywhere else.

THE DESIGN TRAVELS WITH IT. The card carries the region-card, countries-grid and
country-link CSS lifted unchanged — including the RTL chevron rules, where a
"forward" chevron must mirror in Arabic while a disclosure caret must not, and where
[dir="rtl"] .region-card__icon at 0-2-0 would otherwise beat the 0-1-0 --open
modifier. A consumer gets the look, not a lookalike. It needs dir on an ancestor for
that mirroring, which is documented in the file as a requirement.

★ THE CARD OWNS THE DISCLOSURE ROW, after the first cut got it wrong. I put the
"View country posters" toggle in the consumer's slot — and immediately had to copy
the footer-link styling and the rotating caret back into the plugin, re-creating the
duplication this extraction exists to remove. A disclosure inside a card is a card
feature; only the revealed CONTENT is the consumer's, so PosterWall stays in the
slot with its fourteen props and does not become every consumer's API.

★ THE CHEVRON IS INLINE, NOT A <use href="#id">. The plugin declared the symbol once
and every card referenced it. A shadow-root id is scoped to that root, so the same
card mounted elsewhere would reference a symbol that does not exist — and an
unresolved <use> renders NOTHING, silently. The plugin's own comment warns about
that class for external hrefs; a cross-root id fails the same way.

check:regions-uses-library is registered and asserts BOTH halves: the plugin imports
from @map/ and renders the component, AND no longer holds the markup or the CSS it
delegated. A reusability claim only a human can audit is one that rots; this one
fails the day someone re-inlines a copy. Two negative controls keep it honest — the
plugin still uses its own .rl-root classes, and the other tab is asserted NOT to use
the card yet, so the check describes what is extracted rather than what I hope for.

Two of my own mechanical edits produced valid JS and broken content on the way here,
both caught by checking rather than by the parser: a line-numbered splice that
dropped PosterWall's closing /> (fixed by locating boundaries by CONTENT), and a
line-filtered CSS deletion that left 9 orphaned declarations and braces at 12/21
(fixed by removing rules brace-aware, then re-validating the concatenated stylesheet
rather than trusting check:sfc-parse, which only proves the JS parses).
2026-09-27 03:26:36 +00:00
Doxa Maps
60e5fdcf42 The map learned to name peoples, not just their languages
?codes= filters by LANGUAGE, which is what a partner's list usually is and the wrong
filter for a list of peoples. The reach table measured the gap in its own rows: its
Mandarin row counts 36 groups, those 36 speak 30 codes, and 55 Doxa groups speak those
30 — so a link from a 36-group row drew 55 pins. Akha's 33 drew 57; SUN's 93 drew 116.
The extra groups are real and also have nothing; they belong to a different row.

So the map takes ?groups=<slug list> and filters by people group. The slug is
doxa.life's own — carried from the roster into the pin rows, and read in the builder
off the prayer URL their API built from it, never constructed from a name, because a
slug this build invented could name somebody else's people. All 369 of those rows
carry a prayer URL, measured before relying on it.

Row and map now agree by construction: 0 of 35 rows disagree, and the Mandarin link
renders "36 / 2,107" on the map against a row claiming 36. The gate asserts exact
equality for a three-slug link and 0 for an unknown slug — the failure that would
quietly turn a partner's 40-group embed into the whole roster.

The longest link is 1,577 characters, SUN's 93 slugs; well inside what browsers carry,
but worth knowing before anyone adds a row with a thousand groups.
2026-09-27 02:40:12 +00:00
Doxa Maps
b20d16d919 Library index: regenerated once for everything studio landing 8 adds to library/ 2026-09-27 02:17:36 +00:00