doxa-maps/plugin-designs/doxa-research-map
Doxa Maps 88fe7509d0 The packet was praying over the numbers the page loaded with, not the live ones
rega RQ3(b): the packet's "Has Prayer" selection showed 50 of 997 where the legend
row said 699. The legend row is right and the packet was wrong.

A pin's prayer count arrives TWICE. The GeoJSON property is whatever was true when
the page loaded; from then on polling writes the current count with setFeatureState,
and prayer-progress.js:94 colours from ['feature-state','peoplePraying'] once polling
is on. So the lit pins a reader counts are feature-state, and pin-selection.js had no
'feature-state' case at all -- it resolved to undefined, to-number defaulted it to 0,
and the comparison selected only the pins whose FROZEN property happened to be above
zero. 50 of them. The other 649 people groups had been prayed for and the packet left
them out, which is the one thing a prayer packet must not get wrong.

resolveExpr and matchesExpression now take the state alongside the properties, and
selectedPinIds reads it per pin off the MAP rather than off the feature -- the source
still holds the GeoJSON it loaded, setFeatureState never touches it. The source is
added with promoteId:'uniqueId', so the id feature-state is keyed by is the same
props[idKey] the selection already uses.

Degrading is deliberate in both directions: before the first poll there is no state
and property expressions still select, so an early packet is thinner and never empty;
getFeatureState throws on an unknown source, so it is guarded and a bad source loses
the selection rather than aborting the print.

ALSO, rega RQ3(a), a candidate and not yet a confirmed cause. prewarmRegionPaint
wrapped any non-feature-reading paint value in a case to make it data-driven,
including a zoom interpolate -- I argued for that explicitly. useMapLayers.js:319
already held the rule and I had not read it: zoom interpolation is kept by putting the
case in the interpolate OUTPUTS, and "the reverse (zoom nested inside a 'case') is what
breaks the layer, so don't do that." That reverse is the shape Mapbox classifies as
composite before reaching for an interpolation factor the expression cannot supply,
which is the class of the interpolationFactor page error. The wrap now skips zoom
expressions too. A missed pre-warm costs a frame; an illegal paint costs the layer.
None of the three properties it touches is a zoom interpolate at boot, so this is not
proof -- it removes the only writer that can build that shape from a runtime value.

check-pin-selection asserts the live count wins over the stale property, that the
stale property alone selects nothing, that a map with no feature-state still selects
on properties, and that a throwing getFeatureState degrades. Negative-tested:
removing the case, not reading the state, and dropping the throw-guard each turn it
red; restored, green.
2026-09-28 13:56:22 +00:00
..
scripts A browser gate refuses to launch outside the browser slot; check:browser-gates-guarded keeps it that way 2026-09-28 08:20:21 +00:00
src C1b: the resource palette moves to library/colors/, and the local colour globs go with it 2026-09-26 08:51:39 +00:00
index.html Complete app-profiles->plugin-designs sweep across comments/docs; build green; layouts kept at root (load-bearing) 2026-09-17 18:18:19 +00:00
index.js Six entries hand-rolled the RTL text plugin; the library already installs it 2026-09-26 13:35:57 +00:00
research-map.instances.json Map engine is one prop: MapLibre drops the search bar, legend rises 2026-09-24 00:43:44 +00:00
research-map.meta.json consolidate docs into documentation/ and deployment shells into deployment/ 2026-09-21 23:23:23 +00:00
research-map.vue The packet was praying over the numbers the page loaded with, not the live ones 2026-09-28 13:56:22 +00:00