dx-shepherd asked for this with a7b12b44 and the reason is exact: every caller of readSourceFeatures is
written to do NOTHING when it finds no features, deliberately, so that a legend row for an off-screen
region degrades quietly instead of throwing. That is why the MapLibre bug produced no error, no console
line and no visual difference except a camera that did not move — and why it survived until a packet
printed the wrong 50 people.
check:pin-selection drives the reader with fixtures for both engines and no map at all:
· the MAPBOX shape (_data IS the FeatureCollection) reads its features;
· the MAPLIBRE shape (_data is { geojson }) reads the SAME features — the regression that shipped;
· a REMOTE source ({ url }) returns [] AND WARNS;
· the serialize() fallback works, a missing source is [] rather than a throw, a bare Feature is one;
· selectedPinIds ANDs its expressions, and with none returns every pin rather than none.
★ NEGATIVE-TESTED, which is the only thing that makes it worth having: run against the reader as it
shipped, it fails with exactly four findings — the maplibre shape reads 0 instead of 3, selectedPinIds
narrows to nothing, "no expressions" returns nothing, and the remote case returns [] silently. Against
the fixed reader it passes. A test that cannot fail on the code it was written for proves nothing.
★ AND THE REMOTE CASE IS NOW A DECISION, not an accident. dx-shepherd asked what { url } should do. It
returns [] and warns once per source: the features live on a server and cannot be enumerated locally, so
"I cannot read this" is a different fact from "there is nothing here". Collapsing those two is precisely
how the MapLibre bug hid for as long as it did, and no caller can tell them apart from an empty array.
Verified: check:pin-selection, check:mjs-parse, check:doc-scripts, check:package-json-unique-keys all
PASS. gates:pre was NOT run clean here — my main clone sits three landings behind origin/main, so a
newer package.json lists check:targets while that file is absent from the working tree; that failure is
my verification setup, not this change, and I checked it on an untouched tree before reporting.
regions-plugin challenged the rule and was right. A BUNDLE reads
`stage: production` when ANY ONE of its profiles is production, so scoping the
loop by b.stage drags every experimental sibling in with it.
Measured: ALL SEVEN violations it reported were false. cities-internet is a
production bundle whose connectivity-list and data-tables are EXPERIMENTAL;
gospel-resources likewise for charts / data-tables / resources-list; and
my-upg-100-list's own poster-wall and upg-100-list are experimental too -- only
upg-100-mfe is production, and it has carried a label all along. With p.stage the
gate reports 0.
THE SAME TRAP A THIRD TIME. I moved off stageOf (bundle-keyed) after catching it
once, and moved to b.stage -- the same mistake wearing a different field name.
The profile's own stage is the only one that answers "is this row production".
THE SELF-TEST HAD A HOLE THAT MADE THE BUG INVISIBLE: every fixture bundle had
exactly one profile, so bundle-stage and profile-stage always agreed and scoping
by the wrong one could not be detected. A fixture where the two CANNOT disagree
cannot test which one you read. Adds a mixed bundle whose production profile must
FAIL and whose experimental profile must PASS; 6/6.
The two meta files in be398ce8f stay. They were a fix for a non-problem -- both
profiles are experimental -- but a named row is better than a humanized id, and
they cost nothing.
The rule is extracted as a pure undeclaredLabels(manifest) so it can be tested
over a fixture without standing up Vite or needing a built tree, and runs before
that setup so `--self-test` works on a fresh clone.
Four planted cases, and the SECOND is the one that matters -- a rule that fails
everything is as useless as one that fails nothing:
production + no label must FAIL
experimental + no label must PASS (the stage scope is the design)
production + a real label must PASS
production + whitespace-only must FAIL (`label: " "` satisfies a null
check and still renders blank)
Wired as check:studio-rail:self-test. Verified the extraction changed no
behaviour: the live gate still reports the same 8 violations it did before.
It exists because the first version of this rule PASSED on a manifest that
contained the very defect it was written for -- humanize(id) meant the rendered
label was never empty, so nothing could fail.
The rail showed tiles nobody had named while this gate stayed green. The obvious
fix is a no-op, and writing it first is how that came out.
THE NO-OP: testing the rail's label for null/empty can never fail, because
rowsFromManifest falls back to humanize(id) (plugin-studio.vue:421,429). A
profile that declares nothing still arrives as "Doxa Simple Map" -- not empty,
and not equal to the bare id either, so the existing bare-id rule misses it too.
humanize() defeats BOTH rules. That is why country-posters could ship a tile
nobody had named.
So the property is the DECLARATION, read from the manifest, not the rendered
label: does this profile's <profile>.meta.json carry a "label"? A humanized id is
a reasonable DEFAULT and a bad NAME -- "Doxa Simple Map" tells a reader nothing
the folder did not already say.
PRODUCTION-STAGE ONLY, deliberately: 15 profiles declare no label today and 14
are experimental -- work in progress nobody has named yet, which is not a defect.
A gate that is red for a condition the repo accepts gets ignored.
Also declares the two labels my own plugin was missing: upg-100-list becomes
"UPG-100 — Zoned Map" and poster-wall "UPG-100 — Poster Wall".
⚠ THIS GATE IS RED ON MAIN TODAY, 8 violations across 4 plugins and 4 owners
(cities-internet 2, doxa-simple-map 1, gospel-resources 3, my-upg-100-list 2).
It must NOT land before those labels do, or it blocks every other agent's
landing for a defect that is not theirs. The two meta files here fix my share;
they reach the manifest only through the full build, which is the build owner's.
internal/ is the build owner's under rule 5B -- source sha to studio, sequenced
after the other three owners declare their labels.
The runner recognises a browser gate from its file, comments included, so the
check's own comment made it one and it went NOT MEASURED on a headless seat.
The comment is reworded; detection is unchanged (stripping comments instead
dropped four real browser gates).
The dev server's first line on a fresh clone was a warning that rudder-engine is
not built, with a go build command and no word that it is optional or needs Go.
A backend can now carry an optional note, printed as information instead; Rudder's
says it is desktop voice-to-text, fine to skip for the maps, needs Go 1.22+, and
points at the desktop HOWTO.
Excluding the two detector files by path also skipped any real import added to
them. The check now matches only a static import statement at the start of a
line or a literal import('playwright') / require('playwright') call, which the
detectors' escaped regex literals never are; only the guard itself is excluded.
Planted a real import in browser-gates.mjs and one in this check: both fail.
internal/lib/browser-gates.mjs holds the regex that looks for a playwright
import, so the check read it as an import once the file was tracked. The guard
and the two detectors are excluded by path, so a real import added to one of
them still fails.
Two defects, both found by the done-card sweep rather than by looking for them.
1. A STALE ARTIFACT WAS GRADED AS A DEFECT, which is this gate's own recorded bug
recurring. BUG-PACKET-SWEEP-GRADED-AN-ACCIDENTAL-SET-OF-GITIGNORED-ARTIFACTS is
marked DONE, but the card was closed without the mechanism changing: the gate
still readdirSync'd every .html with no freshness test. It went red again on 43
packets left from a run three days earlier -- reporting "0 prayer tick boxes"
about files generated before tick boxes existed, so they could not have had
them.
The directory is gitignored (.gitignore:94), so nothing in it reaches main or
another agent; it is one developer's scratch. The gate already knew an EMPTY
directory is not a failure ("a clean checkout has none"); a directory of
LEFTOVERS is the same fact wearing different clothes. A packet older than the
renderer that produces it is now SKIPPED, loudly, with the command to
regenerate it. The renderer's mtime is the reference because that is what
decides the shape a packet should have -- comparing against "today" would
re-fail every morning.
2. THE UNCONDITIONAL --expect-funnel STOPPED BEING TRUE. The comment above it says
every packet comes from packet.mjs "which always passes FUNNEL". That held
until SERIES landed: a country packet DROPS the funnel page deliberately,
because the funnel narrows every people group on earth down to THIS UPG-100
list, which is not a description of a country. So the gate failed a correct
packet.
The expectation now follows the series that produced the packet, identified by
the series' own NETWORKS text surviving into the artifact, and read from
series.json rather than hardcoded -- adding a series with funnel:false must not
require editing this gate. A packet whose series cannot be determined keeps the
strict expectation, because the safe default for "I do not know" is the
stricter check.
Verified: PACKETS-ALL PASS with 43 stale skipped and the country packet accepted;
and a POISON TEST inside the tree (a upg100 packet with its funnel-title removed)
is still caught -- so the relaxation discriminates rather than passing everything.
The first poison attempt ran outside the checkout and failed on a missing strings
catalogue, proving nothing; re-run in place.
internal/ is the build owner's under rule 5B -- source sha to studio, not landed
here.
Only the browser seat runs browsers, and only inside internal/browser-slot.sh.
That rule was remembered, not enforced: an agent ran a gate it took for a
headless source check and it launched Chromium outside the slot.
internal/lib/playwright-guarded.mjs is now the one way a repo file reaches
playwright: its launch(), launchPersistentContext(), launchServer(), connect()
and connectOverCDP() exit 75 (NOT MEASURED) with a message naming
browser-slot.sh unless DOXA_BROWSER_SLOT is set, and browser-slot.sh exports it
for the command it runs, so a bash -c grandchild inherits it. All 72 files that
imported playwright switch to it (internal/lib/browser.mjs and 64 internal gates;
plugin-designs scripts: doxa-research-map 3, my-upg-100-list 2,
research-map-plus-grid 2, research-dashboard 1): a mechanical import swap.
check:browser-gates-guarded (gates:pre) fails a direct import;
bun run browser-gates lists the 97 scripts that launch a browser. isBrowserGate
moves to internal/lib/browser-gates.mjs so the runner and the check share it.
Two findings from upg100's census. check-one-palette read strategy palettes from
library/colors/ only, so a bundle's own src/colors/ strategy (which overrides the
library one for that bundle) was never checked; it now reads both, the bundle's
over the library's, and --self-test plants a private palette. Rudder's
engine-offline notice gave a go build line without saying Go 1.22+ is needed or
that setup-runtime.sh downloads the model; it now says both and points at
bun run desktop:build and the desktop HOWTO.
A plugin's <profile>.meta.json targets (web, mobile, desktop, widget; absent = web)
now decide where it is built. internal/plugin-targets.mjs is the one reading.
desktop:build builds a desktop plugin into .desktop/<plugin>/ with a page that
names its engine, builds its backend as a Tauri sidecar for this machine, and runs
tauri build with a --config override; tauri.conf.json and public/ are untouched.
The shell starts the bundled rudder-engine (RUDDER_AUDIO=browser, RUDDER_HOME in the
app data folder) and stops it on exit. A plugin whose targets leave out web
(Rudder: desktop + widget) is no longer built for the web tree: run-parallel,
generate-tree and check-composite-freshness skip it, and its public/ folder is
removed; the dev server still gives the Studio its corner. check:targets
(gates:pre, with a self-test of five planted violations) validates the values,
widget corners and backend folders and fails if a non-web plugin reaches the built
tree. Measured: desktop:build rudder-voice on Linux produced Rudder_0.1.0_amd64.deb
(8.8 MB) with the app and the engine side by side; the app has not been launched.
rega read the rendered page and found a fourth leak site my gate had passed: a
SERIES=country poster still printed 'UNENGAGED PEOPLE GROUPS', 'Unengaged of West
Africa', 'Series: List of 100 UPG's · map 11 of 28' and a side box calling it one
of a 30-map set. I had reported that PASS to three agents as proof the render was
clean. It was not proof: the gate's vocabulary was a HAND LIST of the phrases I
had already found, so it could not see framing I never enumerated.
ONE SWITCH (dx-shepherd's call). SERIES=country now implies the country zone
source, the series name and the header strings. COUNTRIES=1 is kept as an ALIAS
that resolves to SERIES=country, so runs that predate series.json keep working
and now get the matching packet too. Two parameters for one concept is exactly
what let rega's render take the new packet path with a UPG-100 header.
The five header strings move into series.json: kicker, title qualifier, series
name, side-box heading and body. One file owns the wording.
THE GATE NO LONGER DEPENDS ON WHAT I HAPPENED TO KNOW. Its vocabulary is DERIVED
from series.json -- every string a series declares becomes that series' own
fingerprint, so adding wording polices it from that moment. Strings shared
between series are dropped (they identify nothing) and a 12-character floor keeps
short phrases from matching innocent prose. The hand list is kept and merged for
phrases no series declares, like 'Project 7000'.
It also strips HTML and CSS comments before matching: generate.mjs ships design
notes into the SVG that QUOTE these strings, and a correct country poster failed
on five of them. A detector that does not know the comment syntax of what it
reads attacks the documentation of the thing it protects.
FOUND WHILE WIRING IT, a pre-existing break: COUNTRY MODE HAS NOT RUN AT ALL.
generate.mjs read plugin-designs/country-posters/data/countries.json, but
ddde518a8 moved that data to library/data/country-posters.json and the reader was
never updated -- no file at that path on main, so every COUNTRIES=1 run died on
ENOENT. Both paths are now tried, newest first, with an error naming the command
that writes it. Country mode builds 173 countries for the first time.
(node --check passed on the fix while existsSync was still unimported -- a parse
check is not a will-it-run check, and the free variable sat in the one branch my
tests had never reached.)
Verified: country header now reads 'Unreached People Groups / Pray for Republic
of The Gambia / What is a Country Prayer Map?'; the UPG-100 default header is
unchanged; the stale poster still fails as country with 11 real hits, so the
comment strip did not blind the check; COUNTRIES=1 alias renders correctly; all
four packet gates PASS.
Driver rule 2026-09-28: an export-poster button opens the print dialog with a
preview, never a direct download. PosterWall defaulted posterAction to
'download', so country-posters and key-cities, which set nothing, downloaded.
The default is 'print'. check:poster-action-print (gates:pre) keeps the default
and fails a consumer that opts into 'download' without a
"posterAction-download: <why>" comment. PDF packets and CSV exports are not
poster buttons and stay downloads.
The country posters shipped carrying UPG-100 framing: the UNENGAGED PEOPLE
GROUPS kicker, 'This packet is 1 of {n} UPG-100 lists', the Project
2000/7000/4700/700 narrowing funnel, and a below-map block telling the reader to
divide the set among THIRTY disciples -- coherent for ~30 UPG-100 zones and
incoherent for ~170 countries. Reported by the Driver.
Not a mislabel: there was NO series parameter anywhere, so 'the packet' and 'the
UPG-100 packet' were one thing and nothing could disagree.
series.json declares each series' funnel flag, string overrides and the three
below-map NETWORKS blocks. Both packet.mjs and generate.mjs read it, because the
leak was in TWO independent places -- the packet strings and the poster's own
furniture, which is not a packet string at all, so fixing either alone leaves the
other printing. SERIES absent or unknown resolves to upg100.
The funnel page drops for country: it narrows every people group on earth to THIS
list, which is not a description of a country. Reusing the existing
missing-vision-funnel path means one way for the page to be absent, not two.
Four packet keys are overridden, not seventeen: 12 of the 17 are funnel_* and go
with the page; the 12 prayer points are invariant, because they are about a
PEOPLE and a country poster still prays for the peoples within it. Overrides
apply to every locale on purpose -- an English country kicker is visibly awaiting
translation, whereas inheriting the translated UPG-100 kicker prints the wrong
series fluently.
The country English is a DRAFT from the Driver's dictated shape, flagged
_draft_awaiting_driver; no translations until he signs off.
check:series-isolation asserts the property, with a self-test over 6 known-bad
inputs. It caught a real leak this commit would otherwise have claimed fixed: the
packet's page 1 lifts the whole SVG from the pre-rendered poster, so a country
packet built against a stale UPG-100 poster still printed 'Thirty disciples'.
Verified at the rendered layer -- country 11 pages, upg100 12, the delta exactly
the funnel page; upg100 networks byte-identical to the text they replaced.
The gallery added every experimental bundle as a tab, so Rudder's engine probes
(/api/rudder/status, /devices, /events) logged three 404s on every embed of the
gallery. The manifest now carries each profile's meta targets, and the gallery
skips a bundle whose profiles are all corner widgets or declare targets without
"web". Rudder itself contacts its engine only where one can exist: the dev
server, the desktop app, or an explicit apiBase.
regions' headless sweep reported interop row 10 as a KNOWN DEFECT owned by me, while rega had proven the
same path working on both engines. dx-shepherd asked which was true. It is the register that is wrong.
★ PROVEN BEFORE DELETING, because "someone said it is fixed" is exactly how a stale exemption becomes a
hidden live one. Four independent checks on main:
- THE ENTRY'S OWN CLAIM IS FALSE. It said onShowOnMap "answers with go('map') only; it never re-dispatches
doxa:select-zone". It does — and it also writes a `select-zone` ATTRIBUTE first, so a pane that mounts
later still reads the request. The attribute is the stronger half and the entry does not know it exists.
- THE CITED COORDINATE IS WRONG. upg-100-mfe.vue:143 is an unrelated comment about tab rendering; the
handler is at :274. A file:line in a register ages into a false claim the moment anyone edits above it —
the third time this cycle a line number has drifted into fiction (gospel's engine offender moved
1490→1505→1509 the same way).
- BOTH FIXES ARE ANCESTORS OF main: 12a64702 (release the retained request on success — the FIRST press
was losing the zone) and 67e9ede5 (the wall clears its memory when its pane goes inert).
- The receiving side still registers its listener, and rega proved it end to end at 810ffa26.
⚠ AND THE REGISTER'S OWN CONTRACT SAYS THIS SHOULD NOT HAVE HAPPENED. Six lines above it:
"DELETED in the same commit as the owner's fix so main never carries a stale exemption or a red gate on
someone else's bug." The entry outlived its defect by three landings because the deletion was not part of
the fix. A convention that depends on remembering is not a convention; the MAPBOX_KNOWN block right below
records the same rule being followed correctly, which is why that one is empty and honest.
The block stays with the reasoning in it rather than being emptied silently, so the next reader sees what
was checked and why — and the printing machinery is untouched for the next real entry.
A preset page baked only the instance's props, so every default in the profile's
.meta.json config that the instance did not repeat was lost (go-int: upg-neighbor's
five presets dropped pinHint:false). instanceHtml now takes the meta config first
and the props over it. check:preset-config-merge (gates:post) reads every built
preset page and fails on a lost default; the rule is written into
PRESET-PLUGIN-STANDARD.md. Affected before the fix: upg-neighbor only; no
production preset has profile defaults to lose.
rega's contact sheet showed what no gate could: page 3 read as ONE SOLID GREEN BAR. 32 contiguous cells
fill most of row 1 of 52, so the page said "a big green block" where the Driver's intent is "32 boxes lost
in 5,200". Rarity is the whole message and a bar cannot carry it.
⚠ AND THE COMMENT I DELETED SAID THE OPPOSITE, WHICH IS THE PART WORTH KEEPING. It read: "Row-major, so the
shaded run reads as a contiguous block from the top — a scattered shading of the same COUNT would be honest
arithmetic and an unreadable picture." I wrote that reasoning about a picture I had never looked at, and it
was exactly backwards. Scattering is what makes the eye hunt for them, which IS the point.
DETERMINISTIC, NEVER Math.random(): a 32-bit FNV-1a hash of the ZONE ID feeds a mulberry32 PRNG, so the
same packet prints identically every time — two copies of one zone must not disagree — while different
zones get different constellations, so a print run is not 28 sheets wearing the same pattern. Verified: two
consecutive renders produce byte-identical files (md5 febafc1813c6 both times).
Partial Fisher-Yates over the index space rather than shuffling all 5,200, so a large share degrades into
an even spread rather than clumping.
★ AND THE NEW ASSERTION IS THE ONE THAT WOULD HAVE CAUGHT THIS. check-packet-render now measures the shaded
cells' VERTICAL SPREAD as a fraction of the grid height, at the RENDERED layer via getBoundingClientRect —
because a COUNT cannot tell a scatter from a bar. 32 shaded is 32 shaded either way, which is precisely why
every existing assertion stayed green on a broken page.
POISON-TESTED rather than trusted: rebuilding the same 32 cells as a contiguous run makes it FAIL with
"span only 0% of the grid height". The real packet measures 95% (32 cells across 26 distinct rows). The 50%
floor is deliberately loose — a genuine scatter spans nearly the whole page, a bar spans ~1%, and anything
between deserves a human look.
⚠ The poison test first ran on a copy in /tmp and the gate REFUSED it — "outside any checkout, so which
build produced it cannot be checked". A sensible guard, and it meant my first poison run proved nothing;
re-ran inside the tree.
en and ar both PASS at 9 sheets. Page 2 (the empty life grid reading as blank graph paper) is UNTOUCHED and
held for the Driver with the page-order question — shading a reader's life we do not know would be a guess
presented as a fact, which is a product decision, not mine.
Driver decision 2026-09-28: the prayer is kept word for word. The
operational-vocabulary rule now removes exactly the prayer text at
packet-strings.json <locale>.points[0].text (every locale, raw and as a string
literal) from a scanned file before it looks; the same word anywhere else is
still reported. Measured: 4 warnings to 0, and a planted file using the word
is still flagged.
The copyable script snippet 404'd on bun run dev for every production map, while
the iframe snippet worked through leafRewrite. /<b>/<b>.js and /<tree>/<b>/<b>.js
now serve the built IIFE from public/<tree>/; a bundle not built yet gets a plain
404 that says to build it, never the HTML fallback.
The rail showed Gospel x Internet as "board": a row's name fell back to its
profile id unless a hand-kept DISPLAY_NAME table had an entry. The manifest now
carries each profile's .meta.json label (or title / displayName), and the rail
shows it without a trailing aside in parentheses; with no label it shows a name
made from the ids ("Gospel Resources · Charts"). DISPLAY_NAME is an override
only and is empty. check:studio-rail fails if any row would show a bare id.
rega's ?lang=ar pass (KA3, 3d64bcad) found English on a localized map in four places, and the census
behind it explains why no gate saw them. Catalogue coverage is COMPLETE: of the 760 keys my bundles
name across the 21 areas they reach, ZERO are missing from any of the ten non-English locales, and the
per-locale English-identical values are numerals, acronyms and cognates rather than debt. Everything
rega actually read was a string no catalogue ever held.
· THE PROJECTION PILL said "🌐 Globe" / "▭ Flat" in all eleven locales. The icon is now the label.
Neither word has a catalogue entry and authoring ten new strings for a two-state toggle to say what
a glyph already says is the wrong trade — but mapui.enableGlobe / mapui.disableGlobe DO ship in all
eleven, as the ACTION, which is what a button should announce. They become the accessible name and
the tooltip. min-width joins min-height on the pill: the 24px used to come free from the text.
· THE TABLE FOOTER said "Σ 2,107 groups". dashboard.groups has shipped in eleven locales all along
(ar "مجموعة") and this one call site never read it. Written with the same singular/plural test as
the header count so the two cannot disagree at one row.
· THE REGION CHART labelled its bars with the prayer API's English region names. Seven regions is a
closed set, so they are now CATALOGUED — five generated from vendored CLDR by
internal/generate-wagf-region-names.mjs, which is the same data countryName.js already trusts.
Generating is what keeps seventy translations out of my hands. middle_east and
north_america_&_non-spanish_caribbean stay English: M49 145 is "Western Asia" and 021 is "Northern
America", and neither means the region we draw. An English label a reader can match to the map beats
a localized one that names the wrong territory. --check verifies the catalogue against CLDR.
· TOP COUNTRIES labelled its bars with the API's English country names. By aggregation time the
alpha-3 is gone, so countryName.js gains iso3ForEnglishName() — reversing its OWN table, exact where
it matches and silent where it does not — which is precisely the ✓ case its header describes.
One thing found while wiring those: the BAR label was the only chart surface still printing the raw
facet key. The donut, its legend and both tooltips already went through legendLabel(); the bar list did
not, which is why these labels stayed English after the donut was fixed for rega's earlier survey. Both
new facets are projected there, so the two charts cannot disagree about a name again.
English is left EXACTLY as authored. Both projections return the key untouched at locale 'en', which
also keeps the 42 countries whose CLDR name differs from ours reading as they do today. Nothing
localizes a VALUE: data-key, isActive() and every click and filter path still carry the canonical
English, the rule the religion donut already followed.
Verified: gates:pre 12/12, check:i18n-keys-exist / parity / never-translated PASS, and the generator's
own --check PASS. Rendering on ?lang=ar goes to rega.
The India ADM1 boundary is Creative Commons Attribution (CC BY 2.5 IN,
sourced from DataMeet and the Election Commission of India), so the credit
has to reach whoever receives the data. It did not: 6.1 MB of boundary
GeoJSON shipped with no human-readable attribution, and a sidecar for one
file of eight.
data/boundaries/ATTRIBUTION now ships beside the data. Every licence value in
it is verbatim from the geoBoundaries API, queried per boundary -- CC BY 2.5
IN for India, Public Domain for China. No licence wording is ours.
★ IT TOOK TWO ALLOWLIST EDITS, and the first one silently did nothing. The
boundary staging is TWO-STAGE: a pair list gathers into the build pool, and a
separate path list decides what reaches the tree. I added the file to the pair
list, rebuilt, and it still did not ship. An entry in one stage of a two-stage
pipeline is not an entry.
WORLD-ADM0 IS CORRECTED, not closed. This register first guessed Natural
Earth, public domain. It is geoBoundaries gbOpen assembled per country: its
features carry geoBoundaries' own iso3/shapeGroup/shapeName, and the plugin's
data/adm0/PROVENANCE.json names the source and endpoint. That file also states
the real problem -- gbOpen passes each country's UPSTREAM licence through, so
one file carries up to 162 licences and up to 162 attribution duties -- and
its licences table is EMPTY. Which of the 162 are CC BY is unestablished.
Stated in ATTRIBUTION as an undischarged duty rather than papered over.
pplr's row is exp-shepherd's evidence: the PPLR ArcGIS org aggregating IMB,
Joshua Project, ROP and WCD identifiers, publishing no licence at all, and
WCD is a proprietary Brill database.
The India ADM1 boundary is Creative Commons Attribution (CC BY 2.5 IN,
sourced from DataMeet and the Election Commission of India), so the credit
has to reach whoever receives the data. It did not: 6.1 MB of boundary
GeoJSON shipped with no human-readable attribution, and a sidecar for one
file of eight.
data/boundaries/ATTRIBUTION now ships beside the data. Every licence value in
it is verbatim from the geoBoundaries API, queried per boundary -- CC BY 2.5
IN for India, Public Domain for China. No licence wording is ours.
★ IT TOOK TWO ALLOWLIST EDITS, and the first one silently did nothing. The
boundary staging is TWO-STAGE: a pair list gathers into the build pool, and a
separate path list decides what reaches the tree. I added the file to the pair
list, rebuilt, and it still did not ship. An entry in one stage of a two-stage
pipeline is not an entry.
WORLD-ADM0 IS CORRECTED, not closed. This register first guessed Natural
Earth, public domain. It is geoBoundaries gbOpen assembled per country: its
features carry geoBoundaries' own iso3/shapeGroup/shapeName, and the plugin's
data/adm0/PROVENANCE.json names the source and endpoint. That file also states
the real problem -- gbOpen passes each country's UPSTREAM licence through, so
one file carries up to 162 licences and up to 162 attribution duties -- and
its licences table is EMPTY. Which of the 162 are CC BY is unestablished.
Stated in ATTRIBUTION as an undischarged duty rather than papered over.
pplr's row is exp-shepherd's evidence: the PPLR ArcGIS org aggregating IMB,
Joshua Project, ROP and WCD identifiers, publishing no licence at all, and
WCD is a proprietary Brill database.
Both are MIT, which asks for the permission notice to be included with the
software. Tabulator's minified .js carried only "(c) Oliver Folkerd 2025" and
its .css carried nothing; Vue's banner names @license MIT but omits the
permission text. Each now has its upstream LICENSE vendored verbatim beside
it -- fetched, not authored, with the URL and sha256 recorded.
★ AND THE ALLOWLIST IS WHY IT WOULD NOT HAVE WORKED. generate-tree stages
vendor/ by an explicit PATH LIST, so the licences sat in data/vendor and never
reached the tree: staged count 0. Vendoring a notice the staging step drops is
not compliance, it is paperwork. Both LICENSE paths are now listed.
documentation/THIRD-PARTY.md is the register the sweep produced: every
non-original thing dx-maps ships, its source, its licence, whether the licence
or attribution actually travels, and the gap. Compiled by inspecting the
SHIPPED tree rather than the source lists, because the question is what a
recipient receives.
Four rows are left OPEN and marked as such. Two are unverified and need a look
(world-adm0's provenance, the pplr upstream). Two need a decision, not a fetch:
people-groups and the 304 derived tables carry data whose redistribution terms
are not established, 36 MB of it. Attaching a licence I have not confirmed
would be worse than the gap, so those go to the Driver. geoBoundaries is
closable by work rather than judgment -- CC BY needs attribution, and the
.meta.json sidecar pattern already exists here for one file of eight.
The build-time stripper matched only a literal passed directly to
useShadowStyles. Literals appended to shared styles (useShadowStyles(X + `…`))
and named literals (const NAME_CSS = `…`, export const NAME_STYLES = `…`)
shipped their comments verbatim; three plugins' comments reached public bundles
that way (check:public-hygiene). All three shapes are stripped now.
The cloud container pulls the repo live; a pb_hooks/ folder or *.pb.js pushed by any writer would be
server-side JS on the team backend. The container never runs repo hooks (the one sign-in hook is baked
into the image), and this gate keeps a hooks folder from appearing at all. Self-tests on every run.
check:universal-props showed LOCALE 'warn' on research-dashboard and people-groups-grid, both inside Big
Picture. Neither bundle has an en-US of its own any more — the fold put every count through
formatCount(), so both warns cited the SAME line, library/utils/formatCount.js:26, COUNT_LOCALE = 'en-US'.
That line is not a bundle ignoring the reader; it is the held decision written down. NUMERALS ARE HELD —
counts do not follow the locale — so 'ok' is not available here without breaking the policy and the gate
that asserts it. This allows it the way regions-dashboard's popup is allowed, at the SITE.
The site is shared, which is the one thing regions' precedent did not cover. A `bundle:RULE:file` key
would need one entry per importer, so the LOCALE column would warn afresh every time a bundle adopted the
shared helper — the column punishing the fold that made the policy auditable in one line instead of eight
call sites. A `*:RULE:file` key is honoured for every bundle that compiles the file, and ONLY for a path
under library/. That restriction is the whole guard: a bundle's own source can never be wildcarded, so
this cannot be used to clear a column broadly.
Measured on main: 4 LOC-ENUS warns -> 2, both remaining pplr-data-maps' and not mine. research-dashboard
and people-groups-grid read 'allowed', regions-dashboard unchanged.
Negative-tested in both directions, because an exemption that is wider than it reads is the failure here:
a planted toLocaleString('en-US') inside research-dashboard's own composable warns again, and a planted
one in library/composables/useDashboardData.js — shared, imported, but not the allowed file — warns too.
A self-test now also refuses a `*:` entry outside library/, which the lookup would silently never honour.
The capability gate reported five composes refs as "resolve to no known bundle/profile" —
bp-research-header, cities-internet-map, gospel-resources-map, my-upg-100-list-map,
research-dashboard-map. All five were correct data. `composes` accepts a third form besides
'<bundle>/<profile>' and a bare '<bundle>': a registered ELEMENT TAG. Nothing needed renaming and no
author's meta.json was wrong; the resolver was short. 5 unresolved -> 0.
Two things the element form forced, both measured:
· A SELF-REFERENCE is an internal child, not a nested plugin. big-picture composes
bp-research-header, its own element, which resolved back to big-picture (cap-4) and promoted the
bundle to cap-5. Excluding self keeps "nests other plugins" from meaning "has parts".
· A name covering several profiles resolves to the HIGHEST tier among them, and both non-slashed
forms now resolve the same way. A bare bundle name and that bundle's element tag both mean "this
bundle", so taking the first profile for one and the highest for the other made the answer depend
on discovery order. gospel-resources shows it: four single leaves and one cap-4 composite in one
bundle, and losing the 4 silently demotes whoever composes it.
The rule now lives in internal/_composed-tiers.mjs, imported by the generator that WRITES the tier and
the gate that CHECKS it. It was duplicated, and a rule this fiddly does not stay identical in two files;
the drift would have shown up as a gate passing a manifest the generator would refuse to write. The
data stays independent — the gate rebuilds the edges from the <profile>.meta.json files on disk and
checks the published manifest against them.
With the count at 0, an unresolved ref FAILS in both. While five were outstanding a warning was right:
they were correct data the resolver could not read, and failing would have blamed five authors for a
gap in the tooling. At zero, a typo and a genuine plain child still look identical from here, so the
one thing that must not happen is quietly reading one as the other.
Verified on the tree as it stands: 37 profiles, 25 bundles, 10 declaring composes edges — 10 agree,
0 disagree, 0 unresolved. Each fail arm negative-tested: a one-letter typo in a ref fails; a published
tier that disagrees with its own edges fails; the same composes shape pointing at another bundle's
cap-4 instead of its own element fails, which is what proves self-exclusion is load-bearing rather
than incidental.
One switch makes a laptop hot-reload what someone pushes elsewhere (e.g. cloud agents): a URL needs no
`git remote add`. Credentials in a https://user:token@ URL are never shown in the log or /api/dev-sync.
The first run downloads the pinned PocketBase (the cloud container's version) for this platform, checks its
SHA-256 and extracts it in-process into backend/bin (gitignored). It serves .pb/dev (gitignored) with the
migrations in git and hooks off, as in the cloud. DX_PB_URL proxies /api/pb to a shared PocketBase instead;
DX_PB_PORT moves the local one, and a port already in use is reported once instead of crash-looping.
Opt-in with DX_SYNC_REMOTE (plus DX_SYNC_BRANCH, default main, and DX_SYNC_INTERVAL, default 15 s) in
.env.local. Every interval the dev server fetches the branch and fast-forwards when this checkout is on it
with no modified tracked files and no local commits, so Vite hot-reloads what teammates landed. Otherwise
it never merges, stashes or resets: it logs once and reports "main is N commits ahead, pull when ready" at
/api/dev-sync for a Studio badge.
A server where Rudder is laptop-only (RUDDER_ENGINE=0) no longer starts a
leftover engine binary. If an engine that records its own machine's microphone
is reached from another machine anyway, Rudder shows the laptop notice and
disables Record instead of recording the server.
ShareButton is not a .mcb. It wraps its button in a .share-wrap div carrying
border-radius:50% and a conic-gradient ring on ::before at inset:-2px, so the
button rules missed it and it kept a round rainbow halo inside the square
panel -- breaking the single-card illusion at the corner the eye lands on.
rega measured it at 390 and asked whether square should flatten it. It
should: in this variant the panel owns the outline, so a child may not bring
its own. The round default is untouched.
Gated, with a control asserting ShareButton really does carry a 50% radius,
so the two rules are not decorative.
The dev backend answers /status with the same offline status as /events, and
Rudder reads it on mount and whenever the event stream drops, so a proxy that
buffers the stream no longer leaves it saying "Runs on your computer" on a
server that should run the engine. The local-microphone notice no longer shows
off this computer. The legend progress bar paints its grey band as a rounded
strip and rounds the first and last segment, which the 24px touch targets had
squared off.
The dev backend reports expected=true while an engine the server is set up to
run is down (RUDDER_ENGINE=1 or RUDDER_AUDIO=browser). Rudder then reads "Not
running yet" and says the engine is starting or failed to set up; "Runs on
your computer" remains for a server with no engine at all.
generate-tree derives each profile's tier from its composes edges (composing
anything is capability 4; composing a capability-4 plugin is capability 5). An
explicit type is checked against the derivation and a contradiction fails the
build naming both. check:capability-derived asserts every profile is 1-5 and
agrees. documentation/PRESET-PLUGIN-STANDARD.md describes the one
*.instances.json file a preset plugin declares.
check:universal-props showed regions-dashboard LOCALE = warn on a single
site: the map popup's fixed en-US population. That is a HELD decision, not
an oversight -- their own site renders en-US numerals, it is carded, and
_chk-numerals-held-format-does-not-vary-by-locale ASSERTS it does not vary
by locale, so making it locale-derived would break a gate that exists to
keep it fixed.
Clearing it had only two mechanisms and both were wrong. Changing the code
reverses a Driver-backed ruling. A bundle-wide ALLOW silences every FUTURE
en-US anywhere in regions-dashboard -- clearing a column by going blind.
So ALLOW is now site-scopable: a `bundle:RULE:file` key exempts the one file
that earned it, falling back to the old `bundle:RULE` form. The entry records
the card and the reason.
Verified in both directions: the column reads `allowed` instead of `warn`,
and planting a new operative en-US in regions-list.vue puts it straight back
to `warn` naming that file. The exemption is one file wide, not one bundle.
`allowed` is the honest terminal state here, not `ok`. `ok` would mean the
numerals follow the locale, and they deliberately do not.
Every plugin is capability 1 to 5, so the rail files each bundle under its
highest capability again. A bundle not yet in production says Experimental on
its own rows. check:studio-rail now asserts there is no section outside 1-5.
ShareButton is not a .mcb. It wraps its button in a .share-wrap div carrying
border-radius:50% and a conic-gradient ring on ::before at inset:-2px, so the
button rules missed it and it kept a round rainbow halo inside the square
panel -- breaking the single-card illusion at the corner the eye lands on.
rega measured it at 390 and asked whether square should flatten it. It
should: in this variant the panel owns the outline, so a child may not bring
its own. The round default is untouched.
Gated, with a control asserting ShareButton really does carry a 50% radius,
so the two rules are not decorative.
check:universal-props showed regions-dashboard LOCALE = warn on a single
site: the map popup's fixed en-US population. That is a HELD decision, not
an oversight -- their own site renders en-US numerals, it is carded, and
_chk-numerals-held-format-does-not-vary-by-locale ASSERTS it does not vary
by locale, so making it locale-derived would break a gate that exists to
keep it fixed.
Clearing it had only two mechanisms and both were wrong. Changing the code
reverses a Driver-backed ruling. A bundle-wide ALLOW silences every FUTURE
en-US anywhere in regions-dashboard -- clearing a column by going blind.
So ALLOW is now site-scopable: a `bundle:RULE:file` key exempts the one file
that earned it, falling back to the old `bundle:RULE` form. The entry records
the card and the reason.
Verified in both directions: the column reads `allowed` instead of `warn`,
and planting a new operative en-US in regions-list.vue puts it straight back
to `warn` naming that file. The exemption is one file wide, not one bundle.
`allowed` is the honest terminal state here, not `ok`. `ok` would mean the
numerals follow the locale, and they deliberately do not.
package.json and check-universal-props.mjs both conflicted only because
main already carries the work: studio landed check:square-map-controls and
my LOC-ENUS refinement, so main's copies are the right ones and were taken
whole.
I broke package.json first, trying to dedupe programmatically: my rule
matched on indentation + key name, which removed a key that legitimately
appears in BOTH the scripts object and the //scripts-adjacent docs object.
Restored from origin/main rather than hand-repaired. 286 scripts, no
duplicate keys, and every entry of mine present -- they all went out with
commits that have landed.
Under bun, process.execPath is bun, and bun --check runs the script instead of
parsing it; a cloud build hung inside check-link-rot-sample.mjs. The gate now
uses node under bun and fails plainly if node is missing.