Commit graph

7 commits

Author SHA1 Message Date
Doxa Maps
06a48a9b42 plugin-designs: neutral comments in shared/ownerless maps
Rewrite code comments across doxa-research-map, upg-neighbor,
research-map-plus-grid, rudder-voice, people-groups-grid, big-picture,
prayer-walk-planner and all-people-groups to drop internal role names
from measurement and provenance notes, keeping each technical fact.
Comments only; behaviour unchanged.
2026-10-01 00:22:13 +00:00
Doxa Maps
9b3eb99550 The number's job was wrong, not its value
rega, row 43 case 3 at 835ec1ac: the all-empty guard catches the cascade on
MapLibre and NOT reliably on Mapbox — in ar/mapbox the single event is still
EMPTY and the grid stays at 2,107. Three of four.

THIS IS THE THIRD TIME I HAVE TUNED A NUMBER FOR THIS ONE DEFECT, so the number
does not get another value. 60ms was "long enough for the cascade" and was not.
3 x 60ms was "long enough for the cascade" and is not either. I cannot measure
the cascade from inside the element, and every value I pick is therefore a
measurement of whichever machine I picked it on — which is the lesson already in
COMMON.md, arriving a third time because I applied it to the duration and not to
the DESIGN.

So the number's JOB changes. It was a guess at how long the cascade takes, which
is unknowable from here. It is now a CEILING on how long an empty report waits
before it is believed, which is a choice — and the two fail in opposite
directions. Too short reintroduced the bug three times; too long merely delays a
genuine clear by an amount no reader perceives. The loop now re-reads until the
state is NON-EMPTY, which is the condition that actually matters, and gives up
only at the ceiling.

900ms, chosen to sit under the composite's 1.2s push window so the two cannot
interact, and to be generous rather than tight — the failure mode of generous is
invisible.

Driven through the rule outside the browser: a cascade that fills at 400ms reports
FILLED at 420, one that fills at 800ms reports FILLED at 840, and a genuine clear
reports empty at 960. The old budget gave up at 180.

check:language-code-link's section G already asserted BOUNDEDNESS, and it failed
when I changed the shape — correctly, because that is the invariant it guards. It
now asserts the ceiling instead of the count, and says in the file why the shape
changed while the invariant did not: without SOME bound a genuine clear is
deferred for ever and the grid can never be un-narrowed. Both arms
negative-tested.

Source only. STUDIO: rebuild doxa-research-map.
NEEDS EYES: rega's ar/mapbox run is the one that matters — the other three already
pass, so a green ar/mapbox is the whole result.
2026-09-27 02:17:35 +00:00
Doxa Maps
8ec207c662 My coalescing broke the echo guard it ran past
rega, row 50: a region row click emitted {region:"asia"} and then an EMPTY event,
identically on both engines. The grid stayed narrowed only because the
composite's 1.2 second push window happened to swallow the second one — and a
pass that depends on a swallow is not a pass. rega said so, and was right to
refuse to score it green.

THE CAUSE IS MINE AND IT IS NOT A TIMING QUIRK. emitFilterState has always opened
with `if (applyingExternal) return`: when the HOST pushes state DOWN through
externalFilters the store changes, and reporting that back up is an echo of the
host's own instruction. That guard worked because the emit ran SYNCHRONOUSLY
inside the watcher, while the flag was still set. My coalescing moved the emit
60ms later, and applyingExternal is released on nextTick — so by the time the
emit ran the flag was false and the echo went out.

I fixed a burst and broke a guard that depended on the burst's timing. Changing
WHEN something runs is a change to everything that depended on when, and I did
not re-read what else was standing on that synchrony.

So the decision moves to schedule time, where the flag still means what it says:
a change that arrived from the host is not a change the reader made, and is not
reported back. The check inside emitFilterState stays as a second line of
defence.

This is the same shape as the dialect case on the branch below, and it is why
that one is a guard rather than a longer timeout: both are "the emit is no longer
where it thought it was".

★ AND THE GATE CAUGHT THE SAME HOLE IN ITSELF FOR THE THIRD TIME. The assertion
searched the scheduler for `if (applyingExternal) return`, and the comment
explaining the fix QUOTES that line — so deleting the code left the gate green.
It strips comments before asserting now. A check its own documentation can
satisfy is not a check, and I have now written that mistake three different ways:
a probe that yields zero when its assumption is wrong, a mutation aimed at one of
three identical bindings, and this.

Source only, stacked on sr/dialect-empty-cascade. STUDIO: rebuild
doxa-research-map (the same rebuild that branch already needs).
2026-09-26 22:06:58 +00:00
Doxa Maps
c53d356d3f The one event was the cascade's empty moment, not the selection
rega, row 43 case 3 at 52eeb0db: the coalescing landed — one event per click,
exactly as intended — and in 3 of 4 runs that one event was EMPTY,
{language:"", languageCode:"", languageCodes:[]}, so the grid stayed at 2,107.
The name fix works whenever it fires; the event just carried nothing to name.

THE CASCADE PASSES THROUGH A CLEARED STATE. mapStore.selectLanguage() sets
selectedDialect = null BEFORE it sets the language, so a dialect click walks the
store through "nothing selected" on its way to the answer. The settle timer fired
inside that window and read exactly what was there. The composite then did the
right thing with what it was told: an all-empty report IS how this element says
"the reader cleared everything".

MY FIRST FIX WAS A TIMEOUT, AND A TIMEOUT WAS THE WRONG KIND OF ANSWER. 60ms was
enough in my seat and not under headless swiftshader. That is the third time I
have tuned a duration on one machine and called it settled, so this one does not
get lengthened — lengthening moves the failure rather than removing it.

The guard is SEMANTIC instead. An all-empty read arriving straight after a filled
one is the one genuinely ambiguous case — it is what a mid-cascade read looks
like AND what a real clear looks like, and nothing in the payload separates them.
So it is re-taken rather than believed: the transient fills in and is reported
correctly, while a real clear stays empty through every re-take and is reported a
few tens of ms later, which no reader can perceive. The re-takes are BOUNDED
(3 x 60ms) so a clear can only ever be delayed, never swallowed, and the whole
budget stays far below the composite's 1.2s push window so the two cannot
collide. The budget resets on every fresh user action.

Note what is NOT assumed: the retries re-read the store on their own timer rather
than waiting for another watcher fire. In the failing runs the watcher fired only
ONCE, so a fix that depended on a second fire would not have worked.

check:language-code-link grows a section G for it, asserting both halves — the
guard exists, and it is bounded. Without the first the transient ships; without
the second a clear can never be reported at all. Both negative-tested.

Source only. STUDIO: needs a rebuild of doxa-research-map.
2026-09-26 22:06:58 +00:00
Doxa Maps
67f5740475 A dialect is called "Arabic, Sudanese", and one click is one event
rega, row 43 case 3, both engines and both locales: clicking a dialect emitted
the right languageCode every time and narrowed the grid in only ONE of four runs,
while the name it reported read ", Afro-Asiatic__Arabic__Sudanese". Two defects
in one click, and both are mine.

THE NAME WAS A TREE NODE ID. selectedDialect.key is family__baseLang__dialect,
and the watcher fifteen lines above the offending template says so in as many
words — I read that comment and then used the key as a display name anyway.
selectedLanguage is also empty by the time a dialect is active, so the template
produced a leading comma in front of an internal identifier: a string no reader
could make sense of, in the one slot meant for a human name. The dialect's
originalLabels are the API labels the legend grouped ("Arabic, Sudanese"), which
are both the right words and already in the reader's locale. When a dialect
groups several labels all of them are reported, because that is what the
selection covers.

THE GRID LOTTERY WAS A BURST MEETING A GUARD. Clicking a dialect does not change
one store value; it clears the language, sets the family and sets the dialect,
across ticks. So the watcher fired three or four times and this element
dispatched three or four map-filtered events for ONE click. The composite ignores
anything arriving while it is pushing state into its children — a guard it needs,
because the grid re-emits during a push and the round trip would otherwise cancel
the selection just made — and that window is 1.2 seconds. Whether the LAST event,
the one carrying the settled state, arrived inside or outside the window was
timing. Three runs lost it; one did not, which is exactly the 1-in-4 rega
measured.

The fix belongs in this file rather than the other end: the guard is correct, and
the burst is what this element should not have been sending. One click, one
event, nothing to drop. A short settle window rather than nextTick because the
cascade crosses ticks (the legend bridge does async work); 60ms is far below the
1.2s guard and below what a reader notices.

I am NOT touching the composite's 1.2s window in this commit. It is now only
reachable by a genuinely rapid second click, and changing a guard that exists to
stop a self-cancelling round trip is not something to do in the same commit as
the bug it was masking.

check:language-code-link grows a section E for both, asserted against the source
because both are shapes. Three mutants, and the SECOND one found a hole in my own
check: searching the whole file for `emitFilterStateSettled()` is satisfied by the
helper's own declaration, so reverting the WATCHER left the gate green while the
burst came back. It now reads the watcher line specifically. A check its own
subject can satisfy is not a check.

Also corrects 22 to 23. rega counted 23 codes under the "Arabic" tier and was
right: 22 came from a cruder comma-split grouping of mine, while the tier's real
three-arm rule selects 23. The gate's computed numbers were always right; only my
prose was wrong. Two more comments still saying 22 live on sr/kit-d1-multivalue-filters,
which is already in studio's queue — I have not rewritten a branch someone is
landing; they need the same one-word change once it lands.
2026-09-26 17:28:46 +00:00
Doxa Maps
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.
2026-09-26 17:28:46 +00:00
Doxa Maps
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.
2026-09-26 13:35:57 +00:00