Commit graph

4 commits

Author SHA1 Message Date
Doxa Maps
9ccec24d29 Generalise the pane re-forward to attribute-configured panes, and gate it
sr asked to adopt usePaneConfig for research-map-plus-grid rather than write a
second fix, and found the one shape it did not cover: people-groups-grid takes
its filters as THREE separate attributes (wagf-region, religion-family,
primary-language) instead of one profile-config blob, so that composite has one
pane of each kind. The helper hardcoded 'profile-config' and gave the grid
nothing.

registerAttrs is the sibling for that shape. It shares the same entries list,
the same watchEffect, the same prune and the same write-only-on-change -- one
mechanism for all four composites rather than a fork per adopter. A helper with
a single caller has not been shown to generalise at all; this is the second
shape that showed it.

Two decisions worth recording. Each attribute is compared and written
independently, so a getter whose region changed does not rewrite the two that
did not and re-trigger the child. And the attribute NAME LIST is pinned at
registration: a later getter that omits a key CLEARS that attribute rather than
leaving a stale filter silently narrowing the grid, which is the same class of
lie as the frozen config this helper exists to fix.

The gate runs the shipped helper with real Vue, not a re-typed copy, because the
dependency-tracking hole found in review is invisible to any test that fakes
reactivity. 28 cases over both shapes. Its subject is the measurement on
development 92ca8b6e6: the outer element reached locale ar by +1.5s while every
nested host carried NO locale key at boot and still none at +15s, across four
composites -- so case 1b asserts that an ABSENT key is still a change.

Case 7 is the adopter's property rather than mine: a re-forward triggered by a
LOCALE change must produce the SAME primary-language string. gridLanguage() can
fall back to a display label, and the grid compares whole normalised values, so
a name fetched in one language matches no row in another. It does not bite today
because the surviving fallback is a value the grid itself produced, but the gate
asserts the class, not today's instance.

Mutation-tested, 7 mutations, all caught: plain array, no write-on-change guard,
skip-instead-of-prune, escaping getter throw, no per-attribute compare, omitted
key left stale, and the label-instead-of-code regression. The first two crashed
rather than naming a case, so cfgOf is null-safe now -- a gate that dies reports
"something went wrong" instead of naming the pane that stopped following.

Registered in package.json scripts and in gates:pre, verified by re-parsing the
file: one key, 104 checks. gates:pre PASS 103 FAIL 0 NM 1.
2026-09-29 18:34:49 +00:00
Doxa Maps
62a41859c9 Guard the helper's unmount hook, and give a non-setup caller a way to dispose
onBeforeUnmount only binds inside setup(). Calling usePaneConfig anywhere else
produced a Vue warning and, worse, no automatic cleanup -- the watchEffect would
outlive its caller. Found while re-running studio's own two-pane falsifier
locally: the test constructs the helper in an effectScope with no component
instance, and Vue said so.

regions-mfe is unaffected either way, because it calls the helper at <script
setup> top level where the hook is valid. The hazard is for the next adopter.

Guarded with `if (getCurrentInstance())`, the pattern useMapData.js:144 already
uses, and `dispose` is now returned so a caller outside setup has something to
call rather than silently leaking the effect.

Studio's falsifier still passes after the change: registering two panes that read
different refs and changing only pane 2's source writes pane 2 and leaves pane 1
untouched.
2026-09-29 17:36:42 +00:00
Doxa Maps
6493872170 Track every pane's getter, prune detached ones, and say when one throws
Studio's 5B review found a dependency-tracking hole by testing the helper with
real Vue rather than reading it. entries was a plain array, so the watchEffect
tracked only what the FIRST registered getter read: with two panes reading
different refs, changing pane 2's source produced ZERO writes, and pane 2 caught
up only when pane 1's source happened to change. That is this helper's own bug
reappearing for any adopter whose panes read different sources. regions-mfe is
safe today because every getter reads the same cfg and spec -- which is exactly
why a reading could not find it.

entries is now shallowReactive, so register() re-runs the effect and every
getter is tracked. Also from the review:
  - stale entries are PRUNED, not skipped: an adopter that rebuilds its pane
    column would otherwise keep detached elements and re-run their getters on
    every pass forever;
  - a getter that throws is reported once per entry instead of being swallowed,
    since silence recreates the symptom this helper exists to fix;
  - the unused `tag` parameter is gone -- a documented check that does not exist
    misleads the next adopter;
  - the host argument now documents that it must be in the SAME tree as the
    panes, because Element.contains() does not cross a shadow boundary and
    passing the custom element would silently skip every write.

regions-dashboard rebuilt so public/ is servable at this sha for the browser
A/B. It is the only bundle that compiles the helper.
2026-09-29 17:32:33 +00:00
Doxa Maps
89bf1feb54 A composite's panes follow the outer profile-config, instead of freezing at mount
regions-mfe built each pane by creating the child element and writing its
profile-config once, inside onMounted, and nothing rewrote it. The pane is handed
a JSON STRING rather than a reactive object, so a later change to the OUTER
element's profile-config updated the composite's own cfg and reached nothing
below it. regions-dashboard is kept alive by v-show rather than remounted, so the
one code path that would have rewritten the attribute never runs again and the
panes stay in the old language for the whole session.

The leaf was never at fault: ProfileLoader's config is a computed over its prop,
lang is a computed over config, and a watchEffect drives appI18n on every change.
Every link is reactive -- it simply never receives a new prop.

library/composables/usePaneConfig.js owns the missing half: a composite registers
each pane element with a getter for its config, and a watchEffect re-forwards
whenever any reactive source that getter reads changes. It writes only when the
serialised config actually differs, because setting profile-config re-parses it
in the child and can remount what it drives -- an unconditional write per tick
would be a rerender loop. Stale elements (a rebuilt pane column) are skipped.

It is a library helper rather than three copies because regions-mfe, upg-100-mfe
and country-posters-mfe all assemble a config and set the attribute once; they
differ only in which keys they inherit, so each keeps its own
INHERITED_PAGE_KEYS and hands the result to the helper. This commit adopts it in
regions-mfe only; the other two are upg100's to adopt.

Two-sided, headlessly: a check that fails when a composite writes profile-config
on a child without re-forwarding names all THREE composites on development, and
on this branch regions-dashboard reports "re-forwarding=usePaneConfig" while the
other two still fail.

NOT VERIFIED IN A BROWSER YET. rega's LIVEDIAG is the falsifier: the nested
[profile-config] should show the OLD locale on development and the NEW one here.
2026-09-29 17:27:25 +00:00