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.