29 commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
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.
|
||
|
|
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.
|
||
|
|
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.
|
||
|
|
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. |
||
|
|
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.
|
||
|
|
30c42b5f46 | Library index: regenerated once for everything studio landing 10 adds to library/ | ||
|
|
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.
|
||
|
|
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.
|
||
|
|
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. |
||
|
|
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. |
||
|
|
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.
|
||
|
|
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). |
||
|
|
b20d16d919 | Library index: regenerated once for everything studio landing 8 adds to library/ | ||
|
|
c2ab779623 |
Kit landing 4a: the shared search, because the facet rail cannot move yet
★ LANDING 4 AS I SCOPED IT IS NOT POSSIBLE, AND THE SPEC WAS WRONG ABOUT THAT. go-int asked for the facet rail next and I agreed. Then the first step of a landing — look before moving — found that DashboardFacetRail imports FOUR plugin-local modules: useDashboardAggregations, useDashboardShell, useTreeLegendColors and useTreeLegendLabels. Moving the rail into library/ would have made a library component import plugin composables, which check:no-cross-plugin-imports rightly refuses. And two of those four are research-specific by my own spec's reckoning — useTreeLegendColors carries the DOXA colour strategies and is supposed to STAY in the plugin — so the rail cannot become library code by moving alone. Its colour and label dependencies have to be inverted into injection first, which section 8 named as a risk and then ordered as if it were free. So landing 4 is three commits, not one, and this is the first: the piece the rail stands on. THE SEARCH WAS NEVER A SHELL CONCERN. buildSearchAccessors, makeSearchMatcher and filterRowsBySearch were exported from useDashboardShell, and EIGHT files reached into the shell to get them — the table, the map panel, the charts rail, the facet rail, the research filters, the top bar, the profile and the config. A module that eight things import is a module, not a corner of another one. Nothing in it is domain-specific: the accessors come from config.facets / config.columns / config.searchKeys, and the matching comes from the already-shared fuzzy.js. Bodies are verbatim; this is a move. The shell re-exports the three names for one cycle, so none of the eight importers changes in this commit — a barrel line rather than a shim file, so there is nothing to delete later, only a line to remove when the eight point at the library directly. The now-dead fuzzy.js import is removed from the shell rather than left. An import nothing uses is the kind of thing that survives three more landings because it looks load-bearing. WHAT THIS DELIBERATELY DOES NOT TOUCH: the shell keeps rowMatchesFilters and the URL-hash escape helpers, which are what check:filter-multivalue reads out of it by path. I checked that before slicing, so no gate path moves in this commit. Library index regenerated in-commit. Source only; no rebuild needed beyond the usual, since every importer still resolves through the shell. |
||
|
|
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. |
||
|
|
29bbab3967 |
Two plugins built the same search highlighter; now there is one
Dispatch work item (5) asked for useTextHighlight in library/. RULE 2 wants a named SECOND consumer before anything moves there, and it exists: regions-dashboard's UupgsList and people-groups-grid both consume the same Fuse-compatible shim, both read match.indices, both wrap hits in <span class="search-highlight">, and both append the tail only on the last range. Line for line the same construction, apart from variable names. So library/composables/useTextHighlight.js exports highlightRanges(value, indices), and UupgsList calls it. Its local escapeHtml goes too: every caller was inside the highlight body that moved. ★ ESCAPING NOW COMES FROM library/utils/html.js, AND THAT IS A CHANGE FOR BOTH CALLERS, stated rather than smuggled. Three implementations existed and differed: library/utils/html.js escapes & < > " ' AND backtick; returns '' for null UupgsList no backtick; guarded null via String(v ?? '') people-groups-grid no backtick; NO NULL GUARD — s.replace(null) THROWS Both deltas move in the safe direction — one more character escaped, one crash removed — and neither changes the output of any value that renders today. The gate pins both, with controls proving each delta is a real difference rather than a no-op: the old copy really does leave a backtick unescaped, and their escaper really does throw on null. people-groups-grid is NOT migrated here: it is another owner's file, so it is handed over as source. The gate asserts it still builds its own highlighter, so this check states the truth rather than implying the job is finished. check:text-highlight is registered (script line, gates:post token, hand-inserted) and negative-tested: an implementation that escaped nothing fails it, and the output is asserted to depend on the indices. |
||
|
|
9816a4244f |
gospel takes the locale-honouring fallback, and it is their call that made it
gospel-plugin's decision, in their words: "most partners will drop a custom
element or an iframe and never install vue-i18n ... an Arabic ministry site
embedding this map serves its readers English while the bundle already carries all
11 catalogues. That is a bug, not a decision."
So all four gospel translators drop `{ fallback: enLookup }` and take the
helper's catalogue default, which resolves through tStatic: the reader's locale
first, English as the last resort. Their three conditions are met and each is
asserted, not assumed:
1. resources-map passes NO namespace — its keys span gospel.* and pins.*;
2. the fallback walks the catalogue ROOT, which tStatic does by construction;
3. params survive, via the null-safe fillParams.
★ THE ROW THEY ASKED FOR, and it is the only thing here that could catch the
regression this migration risked: a BARE host under ar must render
pins.approximate in ARABIC — not the key, not gospel.pins.approximate, not the
English. It reads the SHIPPED ar/ and en/ pins.json and rebuilds tStatic's own
resolution order over them, so it measures the real catalogue rather than a stub.
Three negatives beside it, plus a control that both catalogues really carry the
key, so the comparison is measuring a difference that exists.
enLookup is deleted from all four, and the EN_MESSAGES import from three —
data-tables keeps it, because it still builds a map from EN_MESSAGES.gospel.res at
:182. Checked before deleting rather than after: check:no-free-identifiers would
have caught a wrong removal, since removing something still used creates a name
that is valid JS until it runs.
Two corrections that came from other agents reading this work:
- studio: the JSDoc carried <area>.<key>-shaped examples, which
check:i18n-keys-exist reads as real keys and reports missing. They reworded it
twice because my file copy restored it. Reworded, with a standing note in the
file so a third copy cannot bring them back.
- the register said each site "takes the CATALOGUE fallback, as it always did".
True of the two composables, FALSE of gospel's four, which were moved here by
their owner. A false statement inside a gate message is worse than no message,
because a reader has no reason to doubt it. Now accurate per row.
check:i18n-keys-exist is red on messages.engineNotLoadedDetail. NOT mine and not
touched here: it is in cities-internet/connectivity-map.vue, whose own comment says
the key "arrives with upg100's ade12e4c", and the call already guards for its
absence.
|
||
|
|
1a17d3a211 |
One guarded translator, and the fallback catalogue nobody needed
C4, first wave. Twenty places each stored the same fact — how to translate when the host never installed vue-i18n — and two of them, usePplrT.js and useNeighborT.js, were whole-file near-duplicates at Jaccard 0.82. That pair was the curation audit's evidence that the guard was being COPIED rather than shared. Both now call library/composables/useOptionalT.js and keep their public names, so no call site changes. ★ THE SPEC ASKED FOR (namespace, fallbackCatalogue) AND THE CATALOGUE IS UNNECESSARY. Every copy imported its own English JSON and hand-walked it after stripping the namespace prefix. But library/i18n builds `messages[locale][area]` keyed by the JSON's own filename, and tStatic() already walks that tree and already falls back to English. So `import enPplr from '.../en/pplr.json'` was a second copy of bytes the bundle already had, and the helper takes one argument. ★ AND IT IS NOT A SIZE REGRESSION, which was the one good reason the narrow import might have existed. The build NARROWS library/i18n's catalogue glob per bundle to the areas internal/i18n-areas.mjs finds in that bundle's source graph (check:i18n-treeshake). A bundle that says 'pplr.' gets the pplr area, not the other thirty. Verified before writing the helper, because "the obvious dedup makes every bundle carry every locale" would have killed it. Resolution order, each step earning its place: the host's own vue-i18n when it installed one, so a host managing locale keeps control; then tStatic, which honours the runtime locale and falls back to English; then the key itself, so a missing string is visible rather than blank. The middle step also covers i18n being present but empty — i18n.t() returns the key unchanged, which every copy of this pattern already treated as "not found". Verified by _qa-cycle/02-builder-cycle/_chk-c4-optional-t-equivalence.mjs: 18 assertions across all three host states, with a branch-for-branch equivalence check against the old hand-written implementation and two negative controls. It extracts and executes the module source, because useOptionalT imports vue-i18n and an i18n index that calls import.meta.glob, which plain Node cannot run. |
||
|
|
8fc7e2d62a |
Dashboard kit, landing 2: the layout engine, and the key it was sharing
useDashboardLayout is the second zero-domain piece: it imports nothing but `vue` and reads no row. The four mentions of a people-groups grid in it are comments naming an example of what a host stacks underneath. Body verbatim apart from the one change the spec required before it could be shared, and the same one-cycle re-export at the old path. THE KEY WAS THE REASON THIS COULD NOT JUST BE MOVED. The engine persists the four divider positions to localStorage under a single constant, so every dashboard wrote to the same slot. That was invisible while there was exactly one dashboard — and SHARING THE ENGINE IS PRECISELY WHAT MAKES A SECOND ONE POSSIBLE. Moving it without fixing this would have shipped a collision into the kit's first reuse: drag a divider in one dashboard, reload, and the other has moved. Nothing errors, neither is wrong on its own, and the reader has no reason to suspect the other element on the page. SPEC-DASHBOARD-KIT section 8 named it as this landing's risk; the fix belongs in the commit that shares the engine, not in the one that first trips over it. The key now carries the profile's own id, so a second dashboard gets its own slot without its author knowing this key exists. AND NAMESPACING MUST NOT SILENTLY DISCARD A LAYOUT. Every reader who has dragged a divider has a value under the old shared key; a new key would simply orphan it and reset them for no reason they could see. The engine reads the old key once when the new one is absent. It never writes or deletes it: a migration that also deletes cannot be rolled back, and leaving the old value costs one localStorage entry. check:layout-key-namespaced guards all three. It EVALUATES the storageKey expression the shell actually passes, with two different dashboard ids, rather than retyping the rule — a gate carrying its own copy of the thing it checks passes while the product is broken, which is the lesson from check:linked-vocabulary. Negative-tested: a bare literal fails A, an expression that ignores the id fails B, and writing or removing the legacy key fails C. ★ THE BUS_SRC LESSON WAS APPLIED BEFORE MOVING, NOT AFTER. Landing 1 nearly disarmed check:facet-cascade because that gate reads its subject from a path. I checked first this time: no gate reads useDashboardLayout by path, and the storage-key literal appears in exactly two places, both of them in this diff. The library index is regenerated here, per landing, for the same reason as landing 1 — 101 items now. SOURCE ONLY. Stacked on D1. STUDIO: needs a rebuild of research-dashboard and dashboard-plus-grid. package.json is the parent's copy plus two lines beside check:leaf-engine-per-provider, so D1's registration is preserved. |
||
|
|
50a29b463d |
Dashboard kit, landing 1: the selection bus becomes library code
The first piece of Big Picture to be shared, and it went first for one reason: useSelectionBus has ZERO domain references and imports nothing but `vue`, so it is the only piece that could move with no adapter and no behaviour change. The body is byte-for-byte what main shipped — verified by diffing everything from the first import onward — and only the file's header block is new. A move that also edits is a move nobody can verify, and nine of these have to be verifiable in a row. THE OLD PATH STILL WORKS, as a re-export, for one cycle. That is not politeness; it is what keeps this landing small enough to check. A landing that moved the file AND rewrote every import would be two changes whose failures look identical from the outside. The shim re-exports and does not duplicate: a second copy would drift silently while both halves kept passing their gates, which is the failure check:no-cross-plugin-imports and check:shared-reached exist to stop. It is deleted in the next landing. ★ THE GATE HAD TO MOVE WITH IT, and this is the part a move like this hides. check-facet-cascade reads the bus from a PATH (BUS_SRC) and, for its self-test, copies that file and poisons the copy to prove it can still catch a reverted cascade. Pointed at the shim, it would have copied and poisoned a RE-EXPORT: the mutation would match nothing, the "broken" copy would behave correctly, and the self-test would report that the gate no longer catches anything. One line, and without it landing 1 would have silently disarmed the only check that proves the hierarchy contract. Verified after the move: the gate PASSES at the new path and the self-test still catches a reverted cascade (13 assertions). The library index is regenerated in the same commit, per SPEC-LIBRARY-CURATION section 4: check:library-index compares REGENERATED BYTES, so a new file under composables/ turns it red until `bun run library-index` is re-run. Per-landing regeneration rather than one pass at the end, because nine landings with a knowingly-red staleness gate would train nine readings of "ignore that one". useSelectionBus now appears in the generated menu — which was the whole argument for the existing taxonomy over a new top-level area. NOTHING TO REGISTER IN package.json: the gate that guards this file, check:facet-cascade, was already wired, and check:no-orphan-gates confirms it. This commit does not touch package.json at all, so it cannot conflict with the three landings of mine that do. STILL COMING, deliberately not here: SPEC-DASHBOARD-KIT D1 makes filterSet multi-value. That changes this file's CONTENTS; this changes only its LOCATION. Keeping them apart is what makes "Big Picture is unchanged" provable. SOURCE ONLY — not built, by instruction. Green here on main's tree: check:facet-cascade and its self-test, check:library-index, check:no-orphan-gates, check:no-cross-plugin-imports, check:shared-reached, check:no-free-identifiers, check:mjs-parse, check:sfc-parse, check:descriptions. |
||
|
|
e87db2b0a5 |
Library index: list ApproxPinNote, which landed without a regeneration
check:library-index failed on main after
|
||
|
|
6263c250ec |
C1a: the affinity-block and doxa-region palettes move to library/colors/
First palette migration from SPEC-LIBRARY-CURATION.md. Source only; studio rebuilds (rule 5B — library/ plus four other owners' plugins). WHY THIS ONE FIRST: the duplication was not idle. doxa-research-map's affinity-block.js carried A018 'Tibetan Peoples' and A019 'Himalayan Peoples' — live on the pray-tools API — with colours measured to WCAG 1.4.11's 3.0:1 non-text floor (A013 5.28 unchanged, A018 7.69, A019 3.53; sibling separations 2.61/1.46/1.79). country-posters' copy covered only A001–A017, so 134 rows (6.4% of 2,107) fell to the grey fallback #95a5a6 — the "bunch of grey colours" report. exp-shepherd confirmed that code path headlessly. So the correct copy wins with no judgement call, and the move FIXES country-posters rather than changing it. FOUR CONSUMERS, NOT THE TWO THE SPEC COUNTED, and finding that is the sharpest argument for the migration: research-dashboard/index.js and dashboard-plus-grid/index.js each imported `../doxa-research-map/src/colors/affinity-block.js` — a cross-plugin RELATIVE import into another plugin's PRIVATE folder. Both now import @map/colors/. Only the build found this; no gate names cross-plugin reach. THE GLOB STAYS in both plugins' index.js, deliberately, and is documented there: resource.js is still plugin-local pending C1b, so deleting the glob would unregister it and colour those pins by the soft fallback — the same silent wrong-colour class being fixed here. Deleting the two STALE FILES is what stops the stale copy winning. THE REJECTED-CANDIDATE NOTE MOVED ABOVE `export const PALETTE`, and this is a real constraint rather than tidying: check:one-palette slices from that export to END OF FILE and matches every hex, despite its own comment claiming it reads "only the PALETTE export". #AF7AC5 was rejected for A019 at 2.95 — under the accessibility floor — and mentioned below the export it would have been DEMANDED of every consumer bundle. The note keeps the reasoning without creating the obligation; the gate bug itself is reported separately since internal/ is not mine to edit. The legend needed no change and that is by design: useAffinityBlockLegendData resolves via getColorStrategy(COLOR_MODES.AFFINITY_BLOCK) — the same strategy the pins use — and reads labels from the data, so map and legend gain A018/A019 together. Its comments claiming the strategy lives in a plugin folder were corrected; they now describe where it actually is. GATES: check:one-palette PASS — 8 strategies, 16 bundle/palette pairs (baselined at 6 and 12 before the move, so a red would have been mine). check:library-index PASS after regenerating the derived menu. check:no-free-identifiers PASS. All four consumer bundles build; their artifacts reverted for studio. |
||
|
|
ba6b9229f3 |
Merge main into bundler-1/work (regenerate the tree, re-hash)
Some checks are pending
build-gate / build (push) Waiting to run
Six conflicts, all in generated artifacts under public/, including this bundle's own — another builder's tree regeneration had rebuilt it. Regenerated rather than hand-merged: the three PosterWall consumers rebuilt, then the tree and the hashes. PosterWall.vue itself auto-merged with both sides intact — this branch's embedded/sectionHeads props and main's usePosterPrints refactor — and the four suites that exercise it through the regions poster dropdown were re-run green after the merge, since their change lands inside the component this feature renders. |
||
|
|
61dcd9b28e |
Merge main into bundler-1/work (regenerate the built tree, re-hash)
Some checks are pending
build-gate / build (push) Waiting to run
The nine conflicts were all in generated artifacts under public/. Built files are not hand-merged: every bundle whose source either side touched was rebuilt, the tree regenerated against main's new generate-tree, and the hashes re-baked. Source merged clean — main never touched PosterWall.vue or en/regions.json, they only appeared in a two-dot diff because this branch changed them. |
||
|
|
ddde518a85 |
Regions: expand a region to its country posters, reusing the shared wall
Driver request: "a toggle or carrot added to the bottom of the region section that says view country posters, which will let you view country posters for each of the countries in the regions" — with View-on-map and Print, "the same reusable code from upg 100 list". This is a third sanctioned addition alongside the tab strip and the no-selection chooser. Almost nothing was built. library/components/poster/PosterWall.vue was already the shared, fully parameterised grid, and it already owned the card layout, the "Show on map" header button, the PRINT OPTIONS footer and the client-side raster. regions-list.vue supplies only data and a toggle — the same division country-poster-wall.vue states in its own header. - A "View country posters" toggle at the bottom of every region section, carrying aria-expanded and aria-controls, using the same caret SVG the header and footer links already use, rotated when open. - Opening a region renders that region's countries. Measured: 41 cards for Africa and all 41 previews LOAD (naturalWidth>0 — a 404 still yields an <img>, so the load is the real check). No assets were copied: public/assets/previews already holds 174 country-*.webp and generate-tree already stages them. - "Show Chad on the map" reaches the Region Dashboards tab with title "Chad" and hash "#chad", by mapping PosterWall's CustomEvent listId back through the country-selected the tabs already exchange. - India prints its own reason with the exact number from the data; China renders a real poster. Only India is unsupported — 855 groups and no sheet, against China's 154 on a 3x4. Two defects the screenshot caught and no assertion did: - PosterWall's root is position:absolute;inset:0, because in its own bundles it IS a pane. Embedded in a region card it painted 1440x1100 over the region's country list. Fixed by parameterising the shared component rather than overriding its CSS locally: new `embedded` and `sectionHeads` props, both defaulting to today's behaviour, so country-posters and key-cities are unchanged by construction. Both re-checked. Re-measured: root static, 1200px wide, panel below the grid, no overlap. - With one unnamed group the wall printed a section head reading the literal "Other" plus a count duplicating the region card's own summary. Suppressed by sectionHeads=false. The dataset moved to library/data/country-posters.json now that it has a second consumer, one import line changed in country-posters, and emit-countries.mjs was retargeted so the next generator run cannot recreate the duplicate. The three new strings do not exist on DX-life, so there was nothing to copy and nothing was invented in another language: en only, with the other seven locales falling back to the key. Regression: 15 suites green, including region-vs-live 73/73, country-vs-live 28/28, mobile 44/44 and 26/26, and a new _probe-region-posters.mjs. check:library-index drifted for real this time (3 new library files) and is regenerated here. |
||
|
|
e0fc1b14d9 |
Regenerate the derived library index (3 files, not just mine)
check:library-index went red when wagf-region.js landed, and this time the failure was REAL rather than the mtime artifact it was earlier today — regenerating produced a genuine content diff, where the earlier run produced a byte-identical file. The diff carries three additions, only one of them mine: wagf-region.js, plus printInPlace.js and retryableAsyncComponent.js, which arrived on main without the index being regenerated. So the menu was already describing a library that had moved on. Regenerated whole, since it is derived and partial is meaningless. Worth keeping the distinction: "the gate is red" and "the gate is red because of me" are different claims, and the way to tell them apart is to regenerate and diff rather than to reason about mtimes. |
||
|
|
d9fbff14bb |
Galaxy style on MapLibre, and restore both dashboard resizes
Some checks are pending
build-gate / build (push) Waiting to run
GALAXY + MAPLIBRE. New 'galaxy' style in the shared registry -- deep-space ground, blue oceans -- with both a mapbox and a keyless MapLibre URL, and a paint override that is applied only if the loaded style actually has the layer, so a miss is a silent no-op rather than a break. The dashboard now runs provider 'maplibre' and mapStyle 'galaxy'. Reversible by two config keys; the Mapbox path is untouched. BOTH RESIZES RESTORED. My seven-row change had hard-capped the table's keyhole at 7 x 26px, which read like a default and was not: nothing writes --rd-table-rows, while the drag handles write --rd-shell-h and --rd-map-h. So the cap was unreachable by any gesture and the Driver lost both resizes at once, plus a grey gap where the freed pixels had no consumer. Fixed by restoring the derivation and moving the default instead: the keyhole is once again shell - map - bar - foot, and DEFAULTS.shellH drops 720 -> 602 (map 360 + bar 32 + 7 rows x 26 + foot 28) so the table OPENS at seven rows while staying fully draggable. CSS fallback matched to 602. Measured before the revert: shell handle 849 -> 731 with rows 7, table divider map 360 -> 280 with rows 10, gap 1px after each. Also carries the charts' frozen count column (sticky right:0 with an opaque background, so a scrolled label cannot slide under the number) and the ViewSelect library-index entry. |
||
|
|
18b3366c2b |
Regenerate the library index, which had fallen a day behind
check:library-index was FAILING: the derived menu was 70,718 seconds — about
19.6 hours — older than library/composables/useMapFly.js, which this flight
edited to give fitBounds an explicit duration. The file's own header says
"DERIVED — cannot drift from the code", which is only true of a file that gets
regenerated, and nothing had regenerated it.
The regeneration added one entry, and it is not cosmetic: viewportScrim.js was
missing from the index entirely while being imported by six files —
PosterDialog, SemanticTreeLegend, PosterWall, DataTable, LegendTools and
my-upg-100-list/poster-wall. A real, widely-used utility that an inheritor
reading the library menu could not discover. 85 reusable items now catalogued.
Measured the wider hygiene question while here, since the standing work list
asks for non-reusable components to be demoted out of the shared zone: of the 82
items the index advertises, exactly ONE has zero code importers —
useMapSelection.js. The other 81 are live. So the shared library is in good
shape and there is no demotion sweep to run.
The first measurement said fifteen components were unused, and it was wrong in a
way worth recording. It counted importers among BUNDLES only, so anything
imported by a sibling library component read as dead — PosterPreview.vue has
five importers and would have been demoted. Counting importers anywhere, not
just in plugin-designs/, is what reduced fifteen candidates to one.
useMapSelection is left in place deliberately, because removing a file from the
shared zone is a different kind of change from regenerating a derived doc and
deserves its own commit. It is carded instead. Worth knowing before that
decision: it is 192 lines, unchanged since the initial commit
|
||
|
|
9dc4dda8cf |
consolidate docs into documentation/ and deployment shells into deployment/
Some checks are pending
build-gate / build (push) Waiting to run
Driver 2026-09-21: 'a folder called deployment which will store the capacitor
folder, wordpress folder and tauri folder', and 'a folder called documentation
to store the contributing folder so docs are in one place'.
docs/ + contributing/ + CONTRIBUTING.md -> documentation/ (three doc homes into
one). capacitor/ + wordpress/ + src-tauri/ -> deployment/.
The public/ -> dist/ rename the Driver asked about is NOT here: after seeing the
measured blast radius (855 tracked files, 54 source refs, and public/ is the
served web-root four consumers point at) the Driver withdrew it - 'keep the
public folder the same'.
Path references were rewritten mechanically (258 across 87 files), but three
classes needed hand fixes because a prefix regex cannot see them:
- join(ROOT,'docs',...) in 10 internal scripts - a quoted SEGMENT, not a prefix
- tauri frontendDist '../public' -> '../../public', since src-tauri is now one
level deeper; check:tauri caught this exactly as the card predicted
- the WordPress contract test's dirname(__DIR__,2) -> 3, same reason
Also reverted one over-reach: the rewrite renamed a FINDING-CATEGORY label
'docs' in public-hygiene-check.mjs, which is a rule name in its own header, not
a path.
Verified: full build ALL GUARDS PASS, SRI consistent; check:tauri, check:mobile
(cap sync + webDir OK), check:wp-embed, quoted-scripts, capability-citations,
capability-exemplars, library-index, public-hygiene, parse and drop all pass.
|
Renamed from docs/reference-library-index.md (Browse further)