40 commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
e2cebd9130 |
Shared links keep partner param names, and a CSV export matches multi-value filters
A shared table link can now be written as one query param per facet, and a param name may map to a different bus key, so links already published on partner pages keep working. The CSV export matched no rows once a reader picked a second value in a filter; the default row match now reads a multi-value filter as OR, and the gate no longer exempts that line. |
||
|
|
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.
|
||
|
|
199cc61a19 |
check:no-free-identifiers reads .js modules, which is how the NEUTRAL P1 got past it
The P1 that emptied Big Picture's Religion and Region charts was a free identifier: useDashboardAggregations.js read NEUTRAL five times and never imported it. This gate exists for exactly that class and did not fire, because it walked only .vue files. A bundle compiles .js and .vue alike, so the gate was seeing half of what ships. Two things were needed, and the first attempt got only one of them. Adding .js to the walker is not enough: a .js module was then handed to Vue's SFC parser, which found no <script>, and acorn reported "Unexpected token (2:1)" — the second line of the file's own doc comment. A .js file is its own script block, so it now parses directly, with offset 0 so reported line numbers are the file's real ones. VERIFIED BOTH DIRECTIONS, which is the only way to know a gate works: - clean main: rc=0, no false positives - NEUTRAL import removed again: rc=1, naming useDashboardAggregations.js:173 and :216 So it would have caught the P1 before it reached the Driver's console. Blast radius measured on main first: exactly 2 findings, both false — DOMException and createImageBitmap, standard browser globals missing from the known-globals list. Added. No other owner's file is implicated, and nothing was changed outside this gate. |
||
|
|
be3ee1cdde |
Kit 4d-1: the shared search is declared once, which landing 4a meant to do and did not finish
Landing 4a extracted buildSearchAccessors / makeSearchMatcher / filterRowsBySearch into library/composables/useDashboardSearch.js. It left them DEFINED in useDashboardShell.js as well — 4.3KB of code-identical copies, including the Fuse.js matcher. I compared all three function bodies with comments and whitespace stripped: identical, byte for byte. Nothing had diverged, so nothing failed, and that is exactly why it survived: the facet rail ran the library copy while the shell's KPIs and three panels ran the shell's. The comment that used to sit above those definitions said the point of ONE declaration is that "the searchable text can never drift from what the panels show" — and there were two. This tree has already paid for that shape once, at Jaccard 0.82, with useNeighborT and usePplrT. The shell now imports them like every other consumer, and DashboardChartsRail, DashboardDataTable and DashboardMapPanel read '@map/composables/useDashboardSearch.js' instead of reaching through the shell. No re-export shim: all four consumers are repointed in this commit, so there is nothing for a shim to serve, and an unretired shim is how three older ones sat in the tree unnoticed until this afternoon. check:filter-multivalue grows arm E, deliberately beside arm B. Arm B exists because a copied FILTER predicate lets one panel disagree with the others about what is on screen; a copied SEARCH is the same hazard with a different name. Arm E asserts the library module declares all three, that the shell declares none of them, and that no file anywhere imports them from the shell. Its importer sweep walks plugin-designs and library rather than a hardcoded list, because a hardcoded list of "files that might import this" is the assumption that let 4a's copies survive. Groundwork for 4d-4: this removes 4.3KB from the file that has to be split into a generic kernel and a research composition root, and it removes three exports from the surface that split has to preserve. Source only. research-dashboard and dashboard-plus-grid need a rebuild. |
||
|
|
24b9ada48e |
Plugin Studio and the gallery split production from experimental, from each bundle's own stage
The Driver: a public plugin that is not yet a production map shows under Experimental until it is. A profile's .meta.json declares "stage": "production" | "experimental" (default experimental; a bundle is production if any profile says so) and "heavy": true for a live WebGL owner. generate-tree carries both into manifest.json, so the Studio and the gallery read the same field and neither keeps a list. The seven production bundles are marked: dashboard-plus-grid, research-map-plus-grid, regions-dashboard, my-upg-100-list, doxa-simple-map, cities-internet, gospel-resources. The Studio rail keeps its capability sections for them and puts every experimental bundle (gospel-internet among them) in one final Experimental section. The gallery's auto-discovered Experimental folder skips a production bundle. gospel-internet-board joins HEAVY_ELEMENTS (it can own two WebGL contexts). check:studio-rail asserts the split. |
||
|
|
437e591da8 |
Arabic set after mount never turned the composites RTL, because the instance was read too late
goal (d). Both composites reflect the reader's direction onto their custom-element host with
reflectDir(), and reflectDir read `getCurrentInstance()` at the moment it ran. That call answers only
while setup or a lifecycle hook is on the stack. reflectDir is called from BOTH:
onMounted(reflectDir) -> Vue has set the current instance; this worked
watch(textDir, reflectDir) -> nothing has; the read returned null and nothing was reflected
So a host that declared lang=ar BEFORE mount got dir=rtl correctly, and a host that SWITCHED to ar
afterwards got nothing at all. Arabic then rendered in a left-to-right layout — readable glyphs in the
wrong places, which the comment above this code already names as reading like "a styling quirk rather
than the bug it is".
THAT SPLIT IS WHY IT SURVIVED REVIEW, and it is the part worth remembering: the function is not wrong
and neither caller is wrong; the pairing is. Either line read alone is correct code, and first paint —
the thing anyone actually checks — was right. Only the later switch was broken.
The instance is now captured in setup as _selfInstance, which is the fix research-map.vue already
carries for the same failure in emitFilterState (measured there under Vue 3.5.42).
TWO THINGS DELIBERATELY NOT CHANGED, so nobody has to wonder:
- reflectDir still only ever SETS dir and never clears it. That is documented directly above it — a
host page that declares its own dir owns that decision — so switching ar -> en leaving the attribute
is intended, not a second bug.
- research-map-plus-grid.vue's onMounted(() => { getCurrentInstance()... }) at :422 is CORRECT and is
left alone. Vue sets the current instance for the duration of a lifecycle hook, so that read
resolves. Changing it would be churn, and flagging it would be a gate crying wolf on working code.
|
||
|
|
e514dd8544 |
check:i18n-keys-exist was blind to a translator that arrives from a composable
go-int planted T('poison_key') behind "const T = useGiT()" and this gate reported the tree clean.
That is the one failure a check is not forgiven: a red gate gets fixed, a green gate that should be
red gets believed, and this is the gate that guards goal (d).
Three independent blindnesses were stacked, and any one alone hid the key:
1. the file never writes the word tStatic, so the precondition skipped it before anything ran;
2. useGiT is not tStatic, so the per-file alias discovery found no alias at all;
3. the alias is PREFIXED, because useOptionalT carries a namespace, while its call sites pass BARE
keys -- which the full-key pattern cannot match, because it requires a dot.
A translator bound from a composable is now resolved like any other, and the namespace is READ from
the factory rather than listed here -- useOptionalT and useOptionalI18n directly, plus any
"export function useXxxT() { return useOptionalT('ns') }", which is how useNeighborT and usePplrT
are written. That keeps the next author's factory covered the day they write it, which is the same
reason the wrapper NAMES were already read per file instead of hardcoded. Because qualify() prefixes
a bare key and leaves an already-qualified one alone, one binding registers as both shapes; the two
patterns are disjoint, so nothing is counted twice.
MEASURED before and after, with the same poison on disk: the previous version PASSES it, this one
fails it as "neighbor.zzz_not_a_real_key (via nt())". Coverage went 683 -> 743 full-key and 49 -> 56
prefixed call sites, so 67 call sites were being scored as clean without being read. The tree itself
is still clean: no real key was hiding behind those 67, so there is nothing to route to any owner.
A self-test asserts all three fixes SEPARATELY and exits 3 if any one goes blind, because testing
them together would not say which was fixed -- and because this gate had already been trusted once
while lying.
Empty-namespace bare keys stay outside the model and are COUNTED rather than assumed away. Measured
at zero today: all five such sites pass fully dotted keys, so every one is checked by the full-key
pass. The counter stays because the shape is writable.
Gate and description only. No product code, no rebuild.
|
||
|
|
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. |
||
|
|
bad6991b03 |
D3: the bus can point at a record without choosing it
SPEC-DASHBOARD-KIT D3, and the last of the three decided-not-built items. The bus carried two coordinated channels — filterSet NARROWS scope, selectionSet MARKS individuals — and was missing the third: hoverKey POINTS at the one record under the pointer. It is how a reader sweeps a table and watches the map answer without choosing anything, and kepler.gl treats hover as a configured interaction beside filters for the same reason. IT IS A PLAIN ref AND NOT A SET, AND THAT IS THE WHOLE DESIGN. Hover fires on every mousemove across a 2,107-row table. selectionSet had to become a plain Set with a version counter precisely because a reactive collection fires per element; a single ref has none of that problem — one dependency, one write, no iteration. A repeat is a no-op, so an unchanged value never wakes a panel. HOVER IS NOT SELECTION, AND MUST NEVER BECOME IT. The cheap implementation is to put the key in selectionSet "just while hovering", and every consequence of that is bad while none is obvious at the call site: useSelectionCamera watches selectionVersion, so a hover would fly the camera at every mousemove — the resize-strobe bug wearing a new hat, on a Driver who is photosensitive. The CSV exporter reads selection > filter > all, so a hover would silently change what an export contains. The table's selection pill and the shell's live region both report selection counts, and both would narrate the pointer. So a panel may fade or outline on hover; it may not fly, select or filter. NO PANEL IS WIRED, deliberately, and I want that on the record rather than left to be noticed. D3 says panels opt in, and the first opt-in belongs with a consumer that has a reason: wiring one into Big Picture speculatively would be a visual change to the flagship that no reader asked for, landed while the kit stack is paused. That does leave a channel nothing uses — the same criticism I made of the unmounted filter chips — so the difference has to be earned rather than asserted: this is a documented, gated contract with its first consumer named, not a component that was quietly orphaned. check:hover-channel is what makes it a contract instead of dead code; the first panel to use it inherits the rules instead of inventing them. Three mutants, all caught: routing setHover through selectionSet, dropping the equality guard, and removing hoverKey from the returned surface. SOURCE ONLY. Stacked on landing 2. No rebuild needed — nothing renders it yet. |
||
|
|
15820f1432 |
Dashboard kit, landing 3: the camera and the CSV exporter
Both are zero-domain in the sense the spec meant: they import only `vue` and read rows through accessors. Bodies are what research-dashboard shipped apart from the two parameters the spec named and one policy object, and the old paths re-export for one cycle. ★ THE FIRST STEP FOUND TWO GATES THAT READ THESE FILES BY PATH, which is why it is now the first step. Landing 1 nearly disarmed check-facet-cascade the same way. internal/check-csv-export-shape.mjs does not import useCsvExport; it READS the source and extracts esc() out of it. Left at the plugin path it would have read the one-line shim, found no esc(), and failed with "could not extract esc()" — a gate accusing a working exporter because its subject moved. check-filter-multivalue.mjs — MINE — lists the same file in its PANELS array. Left at the plugin path it would have read the shim, found no predicate to object to, and PASSED VACUOUSLY. That is worse: a gate going quiet about the thing it guards. Both move in this commit. THE COORDINATE IS A PARAMETER; WHAT MAKES A COORDINATE VALID IS NOT. `coords` replaces only the READ of row.longitude / row.latitude, which is pray-tools' spelling and not a universal one. Every guard stays shared: Number() on API strings, 0 accepted per axis, the exact 0,0 null-island sentinel rejected, the out-of-range check. A kit that let each caller re-implement that validation would hand the next dashboard the same four bugs to rediscover. ★ AND THE OPTION WAS DEAD WHEN I FIRST WROTE IT. `coords` was accepted in the signature and never passed to selectionBounds, so a caller could set it, type-check clean, build clean, and watch it do nothing. This tree has paid for that shape before — a defineProps on a component the custom element never binds — and it is worse than a missing feature because it READS as supported. Nobody files a bug against a knob they assume they misused. check:kit-options-reachable now asserts, across all four shared composables, that every option accepted is used. THE CAMERA'S EIGHT CONSTANTS ARE ONE DECISION, so CAMERA_POLICY exports them together. The failure that prevents is a second dashboard re-picking them one at a time and arriving at a camera that feels different from Big Picture's for no reason anybody wrote down. It is read-only and deliberately not yet overridable: making each a per-caller option means threading eight of them through every use, and no consumer has asked for a different camera. Exporting the decision so it can be cited is the half with a use today; saying so is better than shipping eight options nobody passes. THE EXPORTER GAINS exportLabel and sources. "Research Dashboard" was this dashboard's name, and the provenance line said nothing about where the data came from — which is what a provenance line is for. Both default to today's output, to the byte. ★ AND THE NEW GATE HAD THE COMMENT HOLE TWICE IN ONE SITTING. First it searched the raw body, and the comment beside the fixed call site says "`coords` IS PASSED HERE" — so the mutant that removed the argument left it green. Then, stripping comments only per-part, it split the signature's comment PROSE on commas and reported that useCsvExport accepts `and`. Comments are text; every text search has to say so, in both directions. That is the fourth variant of one mistake this week, and all four were caught by running the mutant rather than by reading. Library index regenerated in the same commit, 103 items. Source only. STUDIO: needs a rebuild of research-dashboard and dashboard-plus-grid. The internal/ hunk is one line in check-csv-export-shape.mjs. |
||
|
|
c58db47ac3 |
"Arabic" is 23 ISO codes, not 22
rega counted 23 while measuring the language link, and was right. My 22 came from a cruder comma-split grouping of the labels; the language tier's actual rule has three arms — exact, "base," prefix, " base" suffix — and selects 23. Only the prose was ever wrong. Every computed number in the gate and in the product came from the real rule and has always been 23; these are two comments explaining it, in two files, one word each. I did not correct them at the time because the branch carrying them was already in studio's queue, and rewriting a branch somebody is landing is a worse habit than a stale comment. A comment that states a measurement is a claim like any other, and this is one a reader cannot check without redoing the measurement. |
||
|
|
ca7144a57b |
The gate learns the aliases instead of being told them, and can read main
A fleet-wide census with this gate as it stood would have been worth nothing, and
worse than nothing once mailed to seven owners. Three things had to be true
first.
IT HAD TO SEE OTHER PEOPLE'S CALL SITES. The first version matched tStatic/$t/t
and found four missing keys while sailing past the ten it was written for. Adding
`T` by hand took it to 22 — and that fix was the same mistake one level up. A
census of the tree finds FOUR wrapper names, not one: `const T` x16, `const L`
x6, `const tr` x1 (library/layouts/VerticalTabsLayout.vue) and `const v` x1. A
gate that knows about T and not tr is blind to whichever bundle used the other
name and reports it CLEAN. So the names are now read out of each file, and the
next author's alias is covered the day they write it. Negative-tested by renaming
T to Tz in a file: all six of its call sites are still found.
IT HAD TO RESOLVE THE PREFIX HELPERS IT WAS DECLINING TO. All six `L`
declarations carry a static prefix — four are `tStatic('dashboard.' + key)`, two
are a ladder `tStatic('controls.' + key) || tStatic('dashboard.' + key)`. Those
are ordinary keys with a known prefix, not runtime expressions, and counting them
as unresolved left most of this gate's coverage on the floor: 49 call sites moved
from unchecked to checked, and the unresolved count fell from 80 to 28. A key
resolves if ANY prefix in the ladder holds it, because that is what the ladder
does at runtime.
★ AND THE FIRST RUN OF THAT RESOLUTION INVENTED A DEFECT. BpResearchHeader
renders `L('reset_all', '') || tStatic('controls.reset_all') || 'Reset'` — the
author saying "if this prefix has nothing, fall to the next rung", and
controls.reset_all does hold it. Reported as missing, that is a gate manufacturing
a problem out of a working ladder — the precise failure I was about to mail seven
owners about. An empty English fallback is the author's own signal that the miss
is intended, and is now honoured.
IT HAD TO MEASURE A TREE IT IS NOT STANDING IN. A census has to be taken against
current main; every bundle but mine is stale in my worktree, so a local scan would
report keys another owner fixed an hour ago. A second checkout was not available
either (the box is at 97% disk), so `--rev` reads the files as blobs out of the
object store: one ls-tree and one cat-file --batch, parsed by length rather than
by scanning for a delimiter that could occur inside a file. No worktree, no temp
copy, no writes.
AND IT HAD TO STOP ITS OWN SILENCE BEING READ AS A PASS. This gate asks whether
the keys a bundle NAMES exist. A bundle that names none has nothing to be missing
and passes without a word — and seven do, including two of mine whose composites
render no chrome of their own. Reporting only the misses would let "0 missing
keys" be read as "fully localized", which is the opposite of true for exactly
those bundles. Participation is now counted and printed beside the result, and
the two are never merged into one number.
THE CENSUS, at origin/main
|
||
|
|
f79d8f48e2 |
D1: a filter may be several values, and one function decides what matches
SPEC-DASHBOARD-KIT D1. filterSet[key] may hold an array meaning "any of these";
keys still AND together. The array support is four lines. The reason this is a
landing rather than a patch is the other half.
`a(r) === filterSet[k]` WAS WRITTEN OUT SEVEN TIMES — the shell's filteredRows,
the facet rail twice, the map panel, the table, the charts rail and the CSV
exporter. Seven copies of a one-character rule are survivable while the rule
stays one character. The moment it grows an array case, six of the seven keep
comparing an array with ===, every one of them quietly matches NOTHING, and the
panels disagree with each other about what is on screen. That is not abstract:
the facet rail computes the COUNTS a reader acts on before clicking, so a rail
still using === would print counts for a scope the map and table no longer show.
So the predicate now lives in the bus and every panel calls it. The
de-duplication is what makes the four lines safe.
WHAT A CLICK DOES IS UNCHANGED, deliberately. A plain click still REPLACES. D1
gives the bus the capability; making a rail row additive on an unmodified click
would change what every existing click does to the flagship, which is a product
decision and not this one. The array path is reached programmatically today.
AND THAT PATH IS THE PAYOFF: "Arabic" now narrows the grid. The language tier
covers 22 ISO codes, so no single value expressed it, and the composite had to
send nothing and leave the axis unfiltered — honest, and visibly incomplete,
because the map narrowed and the grid did not. The whole code set now crosses and
the grid ORs it. Verified per tier on the committed fixture rather than on one
example: for all 805 tiers the joined set selects exactly the rows the map
selected.
★ THE SEPARATOR IS `|`, AND MY FIRST DRAFT GOT THAT WRONG IN A WAY WORTH
RECORDING. I joined on `,` and broke label matching outright: 120 of the 864 live
language labels are comma-inverted ("Arabic, Mesopotamian"), so a selection made
BY LABEL was chopped in half and matched nothing. A separator that can also occur
inside a value is not a separator — the rule the URL hash states one file over,
which I had just written and then violated. check:filter-multivalue caught it
before it left this machine, and it caught it because it asserts a behaviour I
was KEEPING (label matching) and not only the one I was adding. Asserting only
the new behaviour would have shipped it.
EMPTY IS NOT A FILTER. An empty array clears the key rather than matching
nothing — a UI deselecting its last chip produces exactly that array, and
treating it as "narrow to zero" is the silent-empty-screen failure the hierarchy
contract already exists to prevent. A one-element array collapses to the scalar
it means, so the hash, the chips and every adapter keep their existing shape
until somebody actually picks two.
THE SURFACES THAT CANNOT SHOW A LIST SAY SO HONESTLY. labelFor resolves each
value and joins them, at the one owner, so the chip bar, the research-filter
chips and the CSV provenance line cannot drift apart. A dropdown shows NO
selection when the key holds several: ticking the first of three would state
something false about the board, which is worse than stating nothing, and the
chip still names every value so the selection is never invisible. The URL hash
carries a list and drops the display slug for it — the slug is one human name for
one code, and there is no honest slug for twenty-two.
check:filter-multivalue runs the real bus, greps the panels for surviving copies
of the predicate, round-trips the hash including a value containing a comma, and
lifts the grid's valueMatches to prove both ends OR the same way. No browser.
Green: check:filter-multivalue, check:language-code-link, check:facet-cascade and
its self-test, check:kpi-unit, check:csv-shape, check:no-free-identifiers,
check:sfc-parse, check:mjs-parse, check:descriptions, check:no-orphan-gates,
check:doc-scripts, check:script-flags, check:quoted-scripts.
SOURCE ONLY. Stacked on kit landing 1, which moved the bus into library/.
STUDIO: needs a rebuild of research-dashboard, dashboard-plus-grid,
doxa-research-map, research-map-plus-grid and people-groups-grid. package.json is
main's copy plus two lines beside check:fits-screen.
|
||
|
|
44a1afba04 |
Decouple prose: no go-base / nuxtbase / nuxtinator / doxa-base names in plugin comments and docs
Comments and prose only, no code change (DECOUPLE-go-base-mentions.md sections B and C, outside library/ and README.md): stack-demo's page-flow note becomes 'a normal host page', the share-link and CORS comments say 'the live test host', embeddability.md's Traefik note says 'a self-hosted reverse proxy' and drops the internal proxy file path, and how-to's hosting option becomes 'a Git host you run (for example Forgejo)'. stack-demo.vue's @description feeds the generated tree, so the landing regenerates the tree. Section A (QA widget), library/ and package.json are left to studio. |
||
|
|
7f9b0d8983 |
Twenty-two keys the code asks for that no catalogue has ever had
A localized CALL SITE is not a localized STRING, and nothing in this tree was
watching the join between the two.
The dashboard's five resize handles read their names through
T('aria.resizeFiltersRail', 'Resize filters rail'). Reviewed, that looks
finished: the call goes to the catalogue and the English is only a fallback.
aria.json held eleven keys and not one of them was a resize key. Every handle
had been announcing itself in English in all eleven locales, and the fallback —
which is right to exist, because a missing key must degrade to readable English
rather than to "aria.resizeFiltersRail" on screen — is exactly what kept it
silent.
No existing gate can see this. check:i18n-parity is satisfied because the key is
missing from ALL locales equally. check:english-leaks and
check:composite-locale-leaks both need the catalogue to HOLD the string before
its appearance counts as a leak, and it does not. check:ui-strings-have-keys
reads templates for typed English. Each is looking at a different end of one
wire. check:i18n-keys-exist asks the question none of them asks: does the key
this code names exist?
It found TWENTY-TWO on its first run, all in this lane — the detail drawer's
five rows, the table's two selection toggles and their hints, the map's loading
line, the breadcrumb, the charts rail and the map legend's aria labels, the
empty-search message, and big-picture's footer. Six of them are the resize keys
on this branch's parent; seventeen are added here, English wording kept exactly
as each call site already reads it, in all eleven locales.
★ THE FIRST DRAFT OF THE GATE WOULD HAVE MISSED THE DEFECT IT WAS WRITTEN FOR.
It matched tStatic/$t/t and reported four missing keys while sailing past the ten
resize keys, because the house pattern is a file-local alias —
const T = (key, en) => tStatic(key) || en — and almost every call site in the
dashboard reads T(...). Matching T( took it from 4 to 22. Prefix helpers like
BpResearchHeader's L(), and any concatenated or template-literal key, are COUNTED
as unresolved and never guessed at: inventing the key set a runtime expression
might produce is how a gate starts reporting keys nobody wrote.
AND BIG-PICTURE, the last HOST-LOCALE failure fleet-wide, gets the two halves it
was still missing. Its title was a hardcoded English literal with no key at all —
the largest text on the element, sitting above an Arabic grid. And it never
reflected `dir`, alone among its siblings, so a host asking for Arabic got Arabic
WORDS in a left-to-right layout: a bug that reads as a styling quirk, which is
why it outlived every locale fix around it. The attribute goes on the
custom-element HOST, because `dir` takes effect through the inherited CSS
`direction` and inheritance crosses shadow boundaries — one attribute covers this
element's chrome and both nested elements beneath it. Only ever set, never
cleared: a host page that declares its own dir owns that decision.
The gate is green with this branch and its parent together, and red without
either. Negative-tested: deleting a key that exists fails naming the key and the
call site; a first argument whose area is not a catalogue file is correctly not
reported.
STUDIO: no rebuild needed for the catalogue, but big-picture must be rebuilt.
package.json adds two lines beside check:i18n-never-translated — a different
region from sr/dashboard-filter-a11y and sr/language-code-link, so the three
merge.
|
||
|
|
f3fcce9386 |
"Arabic" emptied the grid, because no people group speaks "Arabic"
rega, on MapLibre, in BOTH locales: Languages > Language > "Arabic" sent
map-filtered with {language:"Arabic", languageCode:""}, the composite handed the
grid an empty code, and 2,107 rows became 0. An empty grid reads as "no
results" — the worst shape a wiring bug can take, because the reader believes
the data.
ONE LINE, AND IT WAS MINE. The emitter looked for a row whose language LABEL
equalled the selected name. A language tier's label is not a label any row
carries: the legend builds family > language > dialect and derives the language
tier by splitting the API label at the comma. Measured on the live source —
2,107 rows, every one carrying primary_language as {value,label} — there is NO
row whose label is "Arabic". There is "Arabic, Sudanese", "Arabic, Levantine",
"Arabic, Gulf", twenty-two codes in all. The exact match could never hit, in any
locale, which is why it failed identically in en and ar and why the locale fix
that prompted this code was right about the disease and wrong about this line.
This is the derived-value trap one tier down from where I last wrote it up: a
value that crosses a component boundary has to exist on a row. "Arabic" is a
grouping the legend invented; primary_language.label is a field.
So the rows are now matched the way the legend's own node matches pins — base,
"base," prefix, " base" suffix — rather than by a fourth hand-typed spelling of
a rule that already exists in two places. 61 tiers resolved an empty code
before; "Arabic" was one of them.
AND ONE CODE ONLY WHEN THERE IS ONE. "Arabic" means 22 ISO codes, so no single
value expresses it. Emitting one would filter the grid to a fraction of the
selection while looking correct; emitting the tier NAME is what showed zero. The
honest answer is to emit nothing for that axis and leave the grid unfiltered —
the map is narrowed, the grid is not, and the reader can see both. languageCodes
carries the whole set alongside, so this becomes a real narrowing the day
filterSet goes multi-value (SPEC-DASHBOARD-KIT D1) rather than a rewrite.
Measured on the fixture: 764 tiers now resolve one code and filter the grid; 41
cover several and abstain.
A DIALECT IS A LANGUAGE SELECTION TOO, and it emitted nothing at all — the
watcher never observed selectedDialect, so drilling from "Arabic" to "Arabic,
Sudanese" changed the map and left the grid reporting the tier above. Its
originalLabels are exact API labels, so it is the one unambiguously resolvable
case.
check:language-code-link, headless, no network at run time. It LIFTS the
emitter's match rule and the composite's gridLanguage() out of the shipping
source rather than carrying its own copy — a gate with its own copy of the rule
passes while the product is broken, which is exactly how this survived a green
check:linked-vocabulary. It proves the emitter's predicate and the legend's
filter expression agree on every (label, tier) pair, that every tier selects at
least one row, and that an ambiguous tier narrows nothing. The fixture is the
864 distinct label/value pairs from the live source, deduplicated rather than
sampled so the counts above are about the whole set. Four mutants; reverting the
emitter to the exact-label rule reproduces rega's finding exactly — 61 tiers
selecting zero rows, "Arabic" named first.
STUDIO: needs a rebuild of doxa-research-map and research-map-plus-grid.
|
||
|
|
6c804159c3 |
Three browser gates stop passing hollow or racing (rega's gates:post run at ded4b3c1)
- check:dashboard-geocoder-provider: 0 search bars on BOTH engines. Its default leaf (research-map) hides
the bar under MapLibre on purpose, and the tokenless Mapbox run could not draw. It now defaults to
doxa-simple-map (a bar on both engines), and the Mapbox run gets a host token: TEST_MAPBOX_TOKEN, or a
well-formed fake whose token-carrying requests are aborted, never sent.
- check:composite-embed-knobs: every tk case SKIPPED (no baked token since the auto engine), so every
provider case went UNMEASURED and the gate still PASSed. It takes TEST_MAPBOX_TOKEN, and a run where no
case measured anything exits 75 NOT MEASURED instead of PASS.
- check:linked-vocabulary: it attached its groups-filtered listener after goto and could miss the grid's
first emit ("the grid never reported a total"). The totals are now recorded from page start
(addInitScript).
Not browser-run here (browsers are rega's): rega's next gates:post pass is the proof.
|
||
|
|
420c36a405 |
ui-strings baseline: gospel's resources-map types the pin note's first sentence, which now has a key
The shared approximate-pin note (pins.approximate) turned gospel-resources' own hardcoded copy of the same sentence into 'has a key but typed in'. Recorded as known debt for gospel-plugin to switch to ApproxPinNote; no string was added. |
||
|
|
fef8c7b0fd |
The last of the shared-library English: legend, poster preview and tab loading states
Third and final batch of the shared-library strings, in the shepherd's order:
SemanticTreeLegend (4), PosterPreview (2), PosterWall (2) and
VerticalTabsLayout (2), plus PosterWall's "people groups" which already had a
key and was still typed in.
These are all LOADING and OVERFLOW states — "Loading data…", "Showing top 500 of
2,107", "use the geocoder above to find the rest", "Loading map snapshot…",
"preview unavailable", "Could not load X". Text a reader meets exactly when
something is slow or has gone wrong, which is the worst moment to be handed a
language they do not read. PosterWall's own source called one of them "i18n
DEBT" in a comment; it is paid now.
The counted ones take a placeholder rather than being split around an
interpolation — legend.showingTop is "Showing top {n} of {m}" — so a translator
can move the numbers within the sentence. Languages do not agree about where a
count goes, and a sentence assembled from fragments cannot be reordered at all.
VerticalTabsLayout needed a different shape: it is an options-API component, so
a module-level helper compiles fine and then renders nothing, because its
template can only see what the component returns. T is exposed from setup().
★ THE NEW GATE CAUGHT SOMETHING I WOULD HAVE MISSED, on its first real batch.
Adding these keys flipped two strings in
plugin-designs/pplr-data-maps/src/components/SemanticTreeLegend.vue from "no
catalogue entry" to "has a key and is still hardcoded" — that file is a COPY of
the library component I just fixed, carrying the same debt. It is another lane's
plugin so I have not touched it, and it is reported rather than quietly
baselined away. The baseline is refreshed to today's truth so the gate guards
the new line: 285 → 273 without a key, and the two copies recorded as known debt
rather than as a regression.
|
||
|
|
e9f5edf79d |
A gate for English typed into a template, so the ground we have won stays won
check:composite-locale-leaks asks a good question — "does the catalogue
translate this string, and is the page showing English anyway?" — but its
criterion is that the catalogue already HOLDS the string, so a string nobody
ever added is invisible to it by construction. And it runs in a browser, so it
only ever sees what happens to be on one screen.
That blind spot was measured, not guessed. After both composites passed the leak
gate with zero findings, a source audit of the same two trees found 31 more
user-visible strings. Most were aria-labels, titles and tooltips: text only a
screen-reader user or a hovering mouse ever meets. Our localization verification
had been "what can I see", and the blind spot was exactly the readers who most
need the words.
TWO CLASSES, because they are different defects with different fixes:
NO KEY wording must be written and translated — sometimes the Driver's call.
HAS A KEY the translation already ships in eleven locales and the reader is
shown English anyway. The cheapest defect in the catalogue: the
work was done and wasted. Nobody was looking for this one; it only
appeared because the report was split in two.
★ WARN-ONLY AGAINST A BASELINE, DELIBERATELY. Failing on the whole backlog — 285
without a key, 29 hardcoded-with-a-key across 126 components — would make it
unlandable and it would be switched off within a day. It fails on what is NEW, so
the count can only fall; --update refreshes the baseline when a batch lands.
QaCorner is excluded BY NAME, with the reason in the file: an opt-in developer
widget that studio is making host-neutral or removing. Eleven locales for a
corner only we open is not worth the translations, and a judgement like that is
better written down than left as a silent gap.
NEGATIVE-TESTED ON BOTH CLASSES, because a gate never seen to fail is not
evidence: a new text node and a new title attribute are both caught and named;
an aria-label hardcoded to "Reset all" — which already ships in every locale — is
caught as the second class with the fix stated. Restored, it passes.
|
||
|
|
b6abe30698 |
Link language on the ISO code, not the display name, because the name moves with the locale
Localizing the grid's data (
|
||
|
|
e3d25ff6db |
The people-group list is a universal prop: the same maps can show any project's list
Every map showed DOXA's people groups, and only those. The same maps must serve other projects'
lists: Project 7000 (unreached), 4700 (frontier), 700 (frontier of 500,000 or more), 2000 (DOXA).
The list is now chosen the way the engine is:
profile-config `list` > ?list= > window.MAP_LIST > 'doxa' (exactly today's behaviour)
with ids or project aliases ('700', 'project-7000').
- library/lists/lists.json: each list is a FILTER over the IMB / peoplegroups.org dataset already
in the repo (Lostness Priority, Population, Engagement Status), never a hand-kept roster.
- library/scripts/people-groups/build-lists.mjs (`bun run lists`): writes each list's DOXA members
to data/people-groups/lists/<id>.json, joined the way the Vision Card funnel joins, so the
counts agree with it: unreached 7,341 (2,082 DOXA), frontier 4,754 (1,745), frontier-500k 737
(71), unengaged 2,982 (1,929).
- library/lists/peopleGroupList.js: resolvePeopleGroupList and applyPeopleGroupList(rows, cfg).
The default list returns the same array, unfetched. A list whose membership cannot load shows no
groups (fails closed) rather than DOXA's whole list under another list's name.
- Tier A, every production map: DataSourceManager applies it (and the maps that build one pass
their profileConfig), and so does every direct people-group fetch: people-groups-grid, the
Big Picture header, research-dashboard (data, CSV export; the DOXA-wide /statistics baselines
are skipped under another list), Regions (list, dashboards, their table), UPG-100 (4 fetches +
the poster wall), Gospel's roster, and the Experimental upg-neighbor and Wall. The composites
thread `list` to their children like `provider`.
Gates: check:universal-props gains a `list` column (LIST-FETCH, LIST-DSM, LIST-KEYS; its
self-test now plants 9 bypasses) and passes across 18 bundles, with all-people-groups allowed:
it draws the IMB dataset, so its list is Tier B. New check:list-prop-unit (Node, 17 cases, in
gates:post); poisoning the ladder order or deleting a membership file fails it. Documented in
documentation/reference-universal-props.md section 5.
Headless: all 14 touched bundles build; check:composites-mount 20/20 and check:composite-interop
17 links pass on this tree; no-orphan-gates, script-flags, quoted-scripts, doc-scripts pass. Not
rendered in a browser.
|
||
|
|
2a26aefff1 |
A gate for the mistake I shipped: a name used but never bound
I broke both composites on main. Editing them with a pattern-match script, I replaced an import line by matching text that had already changed, so the replacement silently matched nothing and three names — RTL_LOCALES, getRuntimeLocale, detectLocale — were used and never imported. Setup threw a ReferenceError and both elements rendered nothing until a hotfix landed. ★ IT PASSED EVERY GATE, AND THAT IS THE INTERESTING PART. It compiled, because a free identifier is valid JavaScript until it RUNS. It passed my grep of the built bundle for the new code, because the code was genuinely there. It passed template-literal, sfc-parse, composite-freshness and SRI, because not one of them evaluates a setup function. The evidence I had could not distinguish a working composite from a dead one, and I called it "verified headless". So this asks the one question those gates cannot: is every name a component uses actually bound — imported, declared, a parameter, or a known global? It reads the same source the compiler reads, tracks real scopes (module, function, block, catch, class), and runs in milliseconds with no browser. Fleet-wide, over every .vue in plugin-designs/ and library/, because the mistake was a text-editing habit rather than anything about one plugin. WRITTEN BY RUNNING IT, which is the only way an allowlist gets honest. The first run reported 4,259 findings — every one a false positive from a real bug in my own walker: a helper that takes a node TYPE was being handed the NODE, so the function branch never fired and every parameter read as unbound. The second run found the globals this tree genuinely uses (d3, customElements, ShadowRoot, CSS, Function, self, the typed arrays) and vite's build-time defines. The third found a real gap in the gate: an SFC may have BOTH a plain <script> and a <script setup>, and they share a scope — LegendTools exports a function from one and uses it in the other, which I was reporting as free. Reading only one block would have taught the fleet to ignore this gate on its first run. NEGATIVE CONTROL, because a check that has never been seen to fail is not evidence: restoring the broken import makes it FAIL naming all three names and their lines; restored, it passes across 127 components. It is the cheap half of a pair — it proves a name is bound, not that the thing runs. The expensive half is a check that actually mounts a composite. |
||
|
|
57009b7e28 |
Big Picture and Simple Research mount again: import the three locale names they use
|
||
|
|
08f89f55b3 |
Big Picture's header speaks the reader's language, and both composites turn round for it
TWO DEFECTS FROM THE UNIVERSAL-PROPS MATRIX, and they are the same defect at two
levels: a component can be perfectly localized and still show English, and a
page can be perfectly translated and still be laid out backwards.
★ THE HEADER HAD NO i18n AT ALL. Every label at the top of Big Picture was an
English literal passed as a PROP — FilterDropdown translates nothing, it renders
what the caller hands it — so Region, Bloc, People Group and Religion sat in
English above an Arabic page. They now read the catalogue, grid namespace first
because this header drives the cap-2 grid and its sibling reads the same keys.
Reset uses controls.reset_all, so its English becomes "Reset all": an existing
entry rather than invented wording, and arguably more accurate, since it clears
the search too.
Its search placeholder and two aria-labels have no usable entry — the nearest,
grid.search, drops "or countries", which this search actually covers — so they
are on the DECIDE card rather than quietly reworded. They are invisible to
check:composite-locale-leaks by design: its criterion is that the catalogue
already holds the string, and it does not.
★ RTL IS A DIRECTION, NOT A TRANSLATION. createAppI18n reflects an RTL locale
onto document.documentElement, which is right for a leaf page it owns and
useless for an element dropped into somebody else's page — the case these
composites exist for. A host asking for Arabic got Arabic WORDS in a
left-to-right layout: readable glyphs in the wrong place, which reads as a
styling quirk rather than the bug it is.
Both composites now set `dir` on their custom-element HOST rather than on the
layout inside it, and that choice is the fix: `dir` takes effect through the
inherited CSS `direction`, and inheritance crosses shadow boundaries, so one
attribute covers the composite's own chrome AND every nested element beneath it
— including the grid half the audit found bare. Only ever SET, never cleared: a
host page that declares its own dir owns that decision.
VERIFIED HEADLESS: three bundles build; setAttribute("dir","rtl") is present in
both composites and controls.reset_all in big-picture; freshness and SRI clean,
nothing else stale; gates:pre 6/6. NEEDS EYES: Big Picture and Simple Research
at ?lang=ar — header labels in Arabic, and the layout actually mirrored.
|
||
|
|
b5d849f746 |
The map follows the list: filter the grid and the camera goes there
The link ran one way. A choice on the map narrowed the grid — region, religion family and language, verified on both engines — and a choice in the grid moved nothing. A reader who filtered the list down to 83 peoples had no idea where in the world they were. ★ IT IS THE GRID'S FILTERED SET, NOT A ROW SELECTION, AND THAT IS A FINDING. The card says "selecting N rows". people-groups-grid has no row selection — no checkboxes, no selection state — and the desktop pair's fly-to-selection is driven by the DASHBOARD's own data table through its selection bus (useSelectionCamera.js), not by this grid. So building "select rows" here would have meant inventing a control nobody asked for. The grid already reports the slugs it is showing on every `groups-filtered`, so that existing signal is the wire: no new UI, and a grid mounted alone still emits into a room with no listener. The composite sends slugs rather than a bbox because it has no coordinates — this map is the half that knows where anything is, so it frames them itself. CAPPED AT 400, and the cap is the point rather than a performance worry: a larger set is not a selection, it is "almost everything", and framing it would swing the camera to a world view that tells the reader nothing while moving a lot. CAMERA POLICY COPIED, DELIBERATELY, from research-dashboard's useSelectionCamera.js, which worked all of this out for the desktop pair: a single row has a zero-area bbox and would fit at street level for what is really a country-scale centroid; several peoples in one place are the same problem by another route; a set spanning most of the globe animates the wrong way around the antimeridian if framed literally. One shared composable in library/ is the right end state — cited rather than imported because a library move cannot be landed from this lane today, and a later move should delete this block rather than leave a third copy. ⚠ MOTION SAFETY. The Driver is photosensitive, and a camera that re-animates on every keystroke is the strobe class this repo has already paid for. An identical slug set never re-flies, and a reduced-motion preference jumps rather than eases — same destination, no traversal. VERIFIED HEADLESS, per the browser policy: both bundles build, `fitSlugs` appears in the sender and the receiver, SRI and composite-freshness are clean and no other bundle goes stale. The behaviour itself NEEDS EYES. |
||
|
|
29fc335b00 |
Prove the grid-under-map link on both engines, not just the one we happened to run
BUILD-INTEROP-CONTRACT-GRID-UNDER-MAP-BOTH-PROVIDERS asks for a provider matrix:
the same assertions, once per engine, with identical counts. Interoperability
that holds on one provider and not the other is not interoperability.
It is not a formality here. The map half of this link resolves a religion family
through its own legend, and the legend is drawn by the map engine — MapLibre
drops an entire layer in silence for one misplaced paint property, which would
surface as a link delivering nothing while the Mapbox run stayed green.
So the gate now runs the whole measurement under ?provider=mapbox and
?provider=maplibre, and then asserts the two agree: the same starting total, the
same rows for the same family, and the same rows over the composite's wire. A
per-engine pass with different numbers would mean the same choice narrows to
different peoples depending on which engine a partner embedded.
MEASURED, identical to the row on both:
mapbox 2,107 groups · Buddhism 83 direct · 83 linked · mutant 0
maplibre 2,107 groups · Buddhism 83 direct · 83 linked · mutant 0
The mutant still runs first on each engine, so a green here still means the gate
can tell a right mapping from a wrong one — twice over now.
|
||
|
|
412602cdef |
Five more gate scripts read the tree's name from the build config
The research verify-* scripts and research-map-plus-grid's two gates hardcoded 'doxa-maps' (11 literals); they now import TREE_NAME from internal/_tree-name.mjs like the rest of the tooling. |
||
|
|
5639dd1ccf |
Let a host set the language of a whole page, not half of it
Embedded with profile-config {"locale":"ar"}, these dashboards came back half
translated: the map's six tabs in Arabic and the grid's four dropdown labels in
English, on the same screen. That reads as "some strings were never translated",
and it sends whoever investigates to the catalogues — where every one of those
strings is already present and correct.
Two readers, one locale, and only one of them was being told.
t() — vue-i18n, follows the plugin's locale, which ProfileLoader already
drove from the host's config.
tStatic — the plugin-free reader that components mounted as their own custom
element must use (useI18n() throws at a custom-element root and
takes the subtree with it). It resolved the locale from the URL and
the document, which are the only two things a STATIC page can say.
So a locale set at RUNTIME moved every t() string and no tStatic string. Now the
i18n instance publishes its locale whenever it changes — one choke point, so
ProfileLoader, the SPA's picker and the studio's picker all work without any of
them remembering to tell the static reader as well.
★ ON THE WINDOW, NOT IN THIS MODULE, AND THE FIRST VERSION GOT THIS WRONG.
A composite loads SEVERAL bundles — research-map-plus-grid loads its own,
doxa-research-map's and people-groups-grid's — and each carries its own copy of
library/i18n. A module-level variable set inside the map's copy is invisible to
the grid's copy, so the first attempt translated the map and left the grid in
English: the very defect it was written to remove, reproduced one layer down.
Measured, then moved to a window-level store, the way the shared map token has
always crossed bundles. Module state is per-BUNDLE; a locale is per-PAGE.
Both composites also publish the locale themselves, because a composite sits
ABOVE every ProfileLoader on its page rather than under one.
MEASURED with check:host-locale-contract, on
|
||
|
|
471bd05cea |
Stop showing English where the catalogue already holds the translation
A reader on an Arabic page saw a handful of English words among the Arabic:
the table's "People Groups" title, its count line, the Engagement column, the
Gospel Access chart title, and the side drawer's close button. Every one of them
had a correct Arabic entry sitting in the catalogue; nothing looked it up.
THE CHECK CAME FIRST, AND IT REPLACES A SWEEP THAT PASSED A BROKEN TREE.
The old method sorted every rendered string into third-party / data / chrome and
counted only chrome. That split is a guess, and it guessed wrong exactly where
the defect lived: single words like "Region", "Prayer" and "Country" are as
plausible as values as they are as labels, so they scored as data and the sweep
reported zero on a page that was visibly leaking.
check:composite-locale-leaks asks a question with an answer instead:
the English catalogue holds this exact string as a value, AND
this locale holds a DIFFERENT value for that key, AND
the page still renders the English one.
No judgement about what kind of word it is — if the translation exists and the
reader is not getting it, that is the defect. The two surfaces a lookup cannot
judge (vendor map chrome, and table/facet cells carrying API values) are muted
by NAME with a stated reason, never by word, so muting one can never hide the
next real leak that happens to reuse it. The gate failed on this tree naming 9
strings before any of them were fixed.
Also threaded here: both composites now accept `lang` / `locale` in
profile-config and pass it to the children they mount, alongside `tk` and
`provider`. A partner writing {"lang":"ar"} reached nothing before — the knob
was documented and read by nobody.
The prayer facet's label in DashboardResearchFilters is now a catalogue getter
too. Measured before touching it: dashboard.config.js does define prayer_level
and FACETS spreads the config last, so that literal never rendered for this
config — it is a default for a config that omits the facet, and it stays, with
the merge order written down so the next reader need not re-derive it.
Four strings remain English with no catalogue entry at all — "Top countries",
"Select all {n}", the table dock's title, and the singular "group". They need
wording in ten languages that cannot be composed from existing entries, so they
are a DECIDE card for the Driver rather than translations invented here.
|
||
|
|
16629f4906 |
Link religion and language between the map and the grid, by the one table they already share
A reader who picks a religion family on the map now sees the grid narrow to the
same peoples, and a language selection travels the same way. Region already
linked; these two did not, because the two halves do not name a religion the
same way and nothing yet proved what the honest translation was.
The map filters religion through its legend's flat tab, keyed by the FIRST
LETTER of the religion code ('B'); the grid filters through a derived axis that
runs library/colors/religion.js forward per row ('BLA' -> 'Buddhism'). One
shared table, read in opposite directions, copied by neither half — so the wire
carries the family NAME and each side resolves it against its own data.
Two measurements changed the design on the way:
- mapStore.filters.religions, which the map's filter panel writes, is read by
NOTHING: the mapLayers.applyFilters() it was meant to reach is called and
never implemented anywhere in the tree. A link routed through it would have
set state, passed a smoke test and dimmed no pin.
- the map's legend selections are mutually exclusive by design, so it can
light exactly one of region/religion/language. The composite mirrors that
rather than letting the grid hold three facets the map cannot show.
Religion links at the FAMILY and not at the code, deliberately: a map cannot
distinguish 'BLA' from 'BMA' — both dim as Buddhism — so linking at the code
would promise a precision one half cannot keep.
check:linked-vocabulary proves it by measurement rather than by reading: it
compares the total reached over the composite's wire against the total the grid
gives for the same facet on its own (83 of 2,107 both ways), and runs the MUTANT
first — the legend key 'B' instead of 'Buddhism' — failing as NO-TEETH if the
wrong mapping scores the same as the right one. Measured: mutant 0, real 83.
The CLI doc's apk:debug quote was realigned in passing; package.json had gained
app:start-page and the doc still described the older chain.
|
||
|
|
7f700935c5 |
Simple Research + Big Picture: the host's style stops at the box, and the last English goes
TWO FIXES THAT BOTH SHOW UP ONLY WHEN THESE PLUGINS ARE SOMEBODY ELSE'S GUEST. 1. ATTRIBUTE FALLTHROUGH. These SFCs mount AS custom elements, so a style the host puts on the tag lands on the component's own root element. MEASURED with a host style of "display:flex; position:relative; height:100%": that exact string appeared verbatim on the inner root div of people-groups-grid, stack-demo, research-map-plus-grid and dashboard-plus-grid. The gallery sets one on every tab, and the same fallthrough collapsed the Prayer Walk Planner to 76px. inheritAttrs:false on each root — a host styling the TAG is styling the box, not our internals. AFTER: the inner root carries no inline style on any of the four, and each still renders full height (2997-4010px in the harness), so the guard did not become the collapse it prevents. Worth knowing for the next probe: shadowRoot.firstElementChild is a <style> node — Vue injects styles first — so a probe that trusts it measures nothing and reports "no fallthrough, nothing to fix". Take the first non-style child. 2. THE RESIDUAL CHROME the in-gallery re-measure found: the dashboard title, Align Dashboard and its tooltip, World, Vertical Legend, groups in scope, the per-row aria templates (Filter by / Remove filter / groups), and the map drawer's aria label. Catalogued in all 11 locales and read through tStatic, reusing existing wording where the catalogue already had the word. The aria TEMPLATES matter more than their size suggests: they repeat per row, so one untranslated template is dozens of English strings in a screen-reader's ear. |
||
|
|
03b838e16d |
Simple Research: one filter state, so the two halves move together
The composite showed a map and a grid that ignored each other: filtering the grid
to Africa moved its own count 2,107 → 315 while the map beside it kept every pin.
The composite now owns ONE filter state; the children report theirs upward as DOM
events and accept ours downward as attributes, so each stays independently
embeddable — a child mounted alone hears nothing and is handed nothing.
MEASURED on a served built tree, region facet:
grid → map 2,107 → 315, and the map's own state follows (externalRegion and
selectedRegion both 'africa'); clearing returns both
map → grid the map's map-filtered contract → the grid's list 2,107 → 315;
clearing returns it
standalone grid alone 24 rows / Total 2,107 / 0 errors; map alone canvas 1 /
0 errors — unchanged
MUTATION disabling the composite's push leaves the grid filtering itself
(315) while the map stays empty, and kills the reverse direction:
the check fails when the link is broken
Four things this cost, each of which first looked like it worked:
1. StackLayout applies `attrs` ONCE at create — its documented contract. The
first version recomputed `sections`; the composite adopted the filter and the
map's attribute in the DOM never changed. A computed nobody re-reads is not a
channel, so the state is now PUSHED onto the mounted children.
2. The echo cancelled itself. Composite pushes → the grid re-filters → it emits
groups-filtered with activeFilters still EMPTY → the composite adopts that as
"the reader cleared it" and wipes what it just applied. Both handlers were
correct; the round trip undid itself in under a second. A push is not a user
action, so the push window ignores children.
3. The grid ignored host filters BY DESIGN when its own dropdowns are visible:
activeFilters picks local OR props, never both. Right for the desktop
composite, which hides them; silently wrong here. A host facet now seeds the
LOCAL state, so the map's selection appears as a chip in the grid's own bar
and the reader can clear it there.
4. That seeding called lbl(), which is local to another computed — "lbl is not
defined", watcher dead, attribute set, list unchanged. Visible only after the
probe was widened to console.error. A silent watcher failure looks exactly
like a feature that was never wired.
SCOPE, DELIBERATE: region only. The map filters religion and language by the
canonical strings its own data carries; the grid filters them by slugs from a
different table. Linking those means inventing a mapping, and a wrong link is
worse than an honest gap.
The map also gains a verification handle (window.__researchMapFilterState), the
same contract as __rdashMap and __grMap: a linked filter is a STATE claim, and
every DOM proxy for it was either tab-specific or a selector guess.
|
||
|
|
7609cd36f0 |
Composites: pass the provider prop down, so keyless embeds are keyless
The token fix was half the contract. resolveMapProvider's ladder starts at
profile-config.provider, and a child reads the config its PARENT writes — so a
partner asking for
<research-map-plus-grid profile-config='{"provider":"maplibre"}'>
got a child still on mapbox, which then demanded a token. MEASURED on a
third-party origin: "No Mapbox access token provided", 0 tiles, no canvas — for
the exact configuration whose entire purpose is needing no token. Keyless at
volume is why MapLibre support exists; a composite that drops the prop cancels
it, and the failure reads as a token problem rather than a plumbing one.
Both composites now thread `provider` beside `tk`, validated to the two known
values so a typo cannot silently select a third engine.
MEASURED AFTER, partner page, NO token anywhere in the config:
research-map-plus-grid canvas 1280x906, ZERO requests to api.mapbox.com,
basemap painted, 12,379 pins, legend, grid
dashboard-plus-grid canvas present, ZERO requests to api.mapbox.com
Screenshot read: the keyless map also shows the deferred search-bar rule holding
on a partner page — no search bar, and the legend risen into its place with no
gap.
|
||
|
|
dc252bb304 |
Composites: give a partner's embed the token it passes in
A JS embed of either composite mounted a map that could not draw. MEASURED the
way a partner actually embeds — one origin serving the tree with CORS, a
SEPARATE origin serving the page:
research-map-plus-grid "No Mapbox access token provided", style 0, tiles 0, no canvas
dashboard-plus-grid the same
(iframe embeds were fine throughout: style 2, tiles 9, 0 failures)
Both composites declared NO PROPS, so the host's profile-config — token included
— was read by nobody. Our own leaves hide this completely: a leaf carries a
BAKED token and assigns window.MAP_TOKEN, which the shared-token bridge hands to
the nested map. The failure therefore exists ONLY where there is no leaf, which
is precisely the partner page the embed snippet is written for. It is also the
snippet we publish: profile-config='{"tk":"YOUR_MAPBOX_TOKEN"}' is the documented
way to supply a key, and it did nothing.
Both now parse their host config, publish the token page-wide through the shared
composable, and hand it to their children. The two differ in how children are
mounted, so the last step differs:
research-map-plus-grid composes by TAG + BUNDLE URL, so the token is threaded
into the child element's profile-config, where its own ProfileLoader reads it.
dashboard-plus-grid bakes its children in and bypasses ProfileLoader entirely
— and ProfileLoader is the only provider of `mapboxToken` in the repo, so
DashboardMapPanel's inject fell through to its empty default. It now provides
`mapboxToken` itself, from the host's tk with the shared page token behind it.
Spread last and only when present, so a host that supplies none leaves the key
absent and the child asks for exactly what its standalone leaf asks for. A
malformed attribute is caught and ignored: the map half is more useful
token-less than the page is with a thrown setup().
This went in the PLUGINS, not the library. A shared change reaches every map —
that is the lesson SHOW_EXPORT_BUTTON taught this week, and nothing here needs
to be shared to work.
MEASURED AFTER, same harness: research-map-plus-grid canvas 1280x906, style 2,
tiles 9, 0 failures; dashboard-plus-grid canvas present, style 2, tiles 10, 0
failures — basemap, pins, legend, table and charts all drawn. Screenshots read.
Both LEAVES re-checked for regression: canvas 1, 0 failed requests, each still
painting its own map.
|
||
|
|
e7bc86d4f2 |
Map engine is one prop: MapLibre drops the search bar, legend rises
Simple Research showed a Fly + Clusters toolbar where the search bar used to be, and the map offered no way to pick an engine without writing a second map. FLY + CLUSTERS ARE NOW LEAF-ONLY. LegendTools renders at top:14px centred on the legend column — the search bar's exact slot — so on the Regions and Languages tabs (the legendTypes its predicate allows) it reads as having REPLACED the search bar. MEASURED on the leaf: fly toolbar at y=58 against the search bar at y=56, the two on top of each other. In a stack whose bottom half is a grid the reader is scanning, a fly-tour that moves the camera underneath fights the other half, so the composite passes legendTools:false. The standalone leaf keeps both. THE ENGINE IS ONE PROP. resolveMapProvider already read profileConfig .provider, so research-map.instances.json publishes the maplibre instance and a partner running thousands of embeds changes one value instead of maintaining a second map. Mapbox stays the default: it carries the richer feature set. MapLibre is the keyless option for volume. MAPLIBRE SHIPS NO SEARCH BAR, ON PURPOSE. GeocoderComponent's maplibre path still calls makeMapboxForwardGeocode(accessToken) (:526) — the geocoding REQUEST goes to Mapbox whichever engine draws the tiles. Shipping the bar under maplibre would keep billing the exact key the option exists to avoid while LOOKING free. Under mapbox it stays, unchanged. AND THE LEGEND RISES INTO THE VACATED SPACE. SemanticTreeLegend's --stl-top defaults to 60px that exists only to reserve room for a search bar; leaving it when the bar is gone is a dead band. MapToolbar's has-geocoder collapses its own reserved gap the same way. MEASURED against the built tree: leaf mapbox search@56 legend@104 fly@NONE (Prayer) / @58 (Regions) leaf maplibre search@NONE legend@58 — legend rose 46px composite search@NONE legend@104 fly@NONE on Regions 7/7 pre, 18/18 post. KNOWN GAP, pre-existing and NOT introduced here: the mapbox composite still shows no search bar. mapboxToken is injected per element and the composite passes neither tk nor tokenUrl to its child, so the child's geocoder gets an empty token and bails while the tiles render from another path. Isolated but not fixed. |
||
|
|
3685b196cb |
Show the cap-4 composite in the studio's Simple Research slot
The cap-5 shell's "Simple Research" tab rendered an error panel instead of the map, in both trees: Could not load <doxa-research-map> from ../../doxa-research-map/ doxa-research-map.js StackLayout already anchored sibling-bundle srcs to the tree root, but it anchored on the /doxa-maps/ segment of window.location.pathname. The studio mounts at the SERVER ROOT (/index.html), which has no such segment, so the lookup missed and the literal '../../' was returned untouched. The browser resolved it against '/', where '../' cannot climb, and requested /doxa-research-map/doxa-research-map.js — 404, both children, empty stack. That is why it survived: every gate and every screenshot used a LEAF url, and the studio is the one host that is not one. The same composite at its own leaf renders perfectly. resolveBundleSrc now falls back to bundleTreeRoot() — the bundle's OWN script URL, which rollup rewrites to document.currentScript.src in the IIFE build. That is a fact about where the code came from, not a guess about where the page is, which is the lesson stack-demo.vue wrote down when this defect appeared for the third time. It returns null in dev ESM and for any non-<name>/<name>.js layout, so it never guesses. The pathname rule stays FIRST and the untouched-string fallback stays LAST, so every path that worked before is byte-identical: this can still only fix a path, never break one. research-map-plus-grid also adopts stack-demo's bundleSrc() dev/prod helper. In dev, vite serves entries at /plugin-designs/<bundle>/index.js and there is no <bundle>/<bundle>.js to reach, so the dev surface 404'd too. import.meta.env.DEV is statically replaced, so the built output is byte-identical to the literal it replaces. Verified in a browser against the built tree, reading the screenshots: studio root -> Simple Research paints the world map with its prayer legend, on the dev server too. Negative-tested: disabling the new fallback and rebuilding restores the exact 404 and the empty canvas. No regression across every StackLayout consumer — rmpg shallow leaf, rmpg deep default/ leaf (the case the pathname rule was written for), spa -> Simple Research, dashboard-plus-grid, stack-demo — and all seven studio dashboards. 7/7 pre, 22/22 bundles, 18/18 post. |
||
|
|
4c8f6b4a6d |
Share the mobile composite too, and make the url absolute so the embed works
research-map-plus-grid had the same defect fixed yesterday on its desktop
sibling: sharing the composite emitted a link to doxa-research-map, the child it
nests. The gate said it exactly — "share URL reopened <doxa-research-map>, not
<research-map-plus-grid>" — and a recipient got the map alone, losing the grid.
Checked all four cap-4 composites rather than assuming the fix generalised.
my-upg-100-list owns its Share button directly and is fine. big-picture composes
a header plus the grid, owns no map and ships no Share button at all; the gate
reports "window.mapboxgl UNDEFINED" against it, which is its map precondition,
not a share failure — that bundle is out of this gate's scope and I put it in
the probe by mistake. So one real instance, now fixed.
Tier 1 again, for the same reason: this child also hardcodes a prop
(research-map.vue:2149 `:embed-path="'doxa-research-map/research-map/index.html'"`)
and the ladder is `embedPath || cfg?.shareEmbedPath`, so a prop outranks host
config and tier 2 is unreachable. Delivered through the profile-config attribute
rather than Vue provide, because this composite mounts its children as real
custom elements via src — there is no provide chain to reach them.
The url had to be ABSOLUTE, and the first version was not. resolveShareUrl
returns a tier-1 shareUrl verbatim (embedBase.js:95, `return whole.trim()`, no
resolution), and ShareButton feeds the same string into its embed snippet:
<iframe src="{shareUrl}">. A relative '../../…' would have resolved correctly in
the share LINK, because the browser resolves it against this page, and then
404'd in the EMBED, because it resolves against the partner's page instead. That
would have passed the gate — the gate navigates the link — while breaking the
deployment method the link exists for. Tier 1 is documented as "a WHOLE url" for
precisely this reason. Now computed with resolveEmbedBase() so it stays correct
on doxa.life, on nuxtbase.app and in a dropped tree.
Verified the desktop sibling did not have the same latent flaw: it produces a
fully absolute url, confirmed in the live gate output.
smoke:share-all now carries eleven targets. Full suite 11/11 pass · 0 fail.
Eleven gates pass, including sri, composite, composite-srcs and synced-css.
|
||
|
|
236fef5682 |
Give nested ProfileLoader children the profile-config they require
Some checks are pending
build-gate / build (push) Waiting to run
Two composites nested a child by tag and URL without a profile-config. Those children do not render a component directly — their bundle entry mounts ProfileLoader, which reads profile-config.profile to choose which view to load. With none, the element still mounts and still reports its tag, so every structural check said both children were present, while the loader rendered "missing or invalid profile-config prop" where the map should be. The grid half of each page worked, which is how it survived. Found by reading a regenerated README screenshot, not by any gate. The mobile screenshot showed an error card and an empty pane; the fixed one shows the real research map with its data, controls and progress figures. research-map-plus-grid was mine, shipped that way. stack-demo predates it and had the same defect against research-dashboard. Both now pass the shape their own generated leaf pages use, so nested and standalone mounts ask for the same thing. check:composite-srcs now requires a profile-config on any child whose bundle mounts ProfileLoader, derived from which entries import it rather than a list. The first version of that check tested for the word and passed on its own explanatory comment; it now matches the quoted attribute key, and a negative test that mutates only the key fails as it should. Also regenerates three derived artifacts that had drifted behind this flight's changes: the library index, the capability exemplars table, and all twelve README screenshots. |
||
|
|
b57aef457c |
Add the mobile capability-4 composite: research map above the people-groups grid
Some checks are pending
build-gate / build (push) Waiting to run
The desktop composite pairs the research dashboard with the grid. This is its mobile sibling: the monolithic 7-tab research map on top, the same grid beneath, stacked to scroll. Which one a shell shows is an aspect-ratio choice made at the SPA level, so neither plugin knows about the other and both stay embeddable. The grid's own search and filters are ON here. The desktop composite turns them off because the dashboard header owns those controls and drives the grid through props; the research map has no such header, so switching them off would leave the grid with no way to be searched at all. Composes its children by tag and bundle URL rather than importing their .vue files. Importing the research map directly would bypass its bundle entry, which is where its three bundle-private colour strategies, its profile registry, its Pinia and i18n pair and the RTL text plugin are installed — and losing those is a runtime defect in a green build, because a thrown setup() removes the subtree with an empty error. Add check:composite-srcs, which resolves every nested src against the built tree and asserts every nested tag is registered. A wrong relative depth is a 404, and the stack layout swallows a failed section on purpose so one broken child cannot take the whole stack down — which means the cost of that resilience is a silently empty band. Negative-tested both ways: breaking the depth and renaming a tag each fail with the file and path named; restoring returns to pass. |