Commit graph

687 commits

Author SHA1 Message Date
Doxa Maps
bee8d7182c A DOM query argument is a selector, not prose
gospel's diff of a hand triage against the checker found this: a selector
listing two custom elements was reported as user-visible English, because it
reads as English only where the tag names happen to be English words. The
single-tag rule matched neither, since it is a comma-separated list.

Exempt by call site rather than by content, as with developer strings. No
content rule can reliably tell a tag list from a sentence, and the site is a
fact about what the string is for. Covers querySelector, querySelectorAll,
closest, matches and the getElementBy* family.

The self-test plants gospel's own case and a second list of custom elements
from another plugin, since the pattern is not theirs alone.
2026-09-28 12:19:23 +00:00
Doxa Maps
41f66f0ce9 Catch a key that can never resolve, not just a string never translated
A namespaced translator qualifies every key with its own area, so a key from
another area can never resolve and the English fallback becomes the normal path
in every locale. The reader sees English exactly as if it had been hardcoded,
which is why it belongs here rather than in a separate check.

Nothing caught it before. The existing key check reads the key as written,
which does exist; it is the key as qualified that misses. exp-shepherd hit it
twice in one day, in two different plugins.

Detected by shape -- qualified key absent while the unqualified one is present
-- so it cannot fire on an ordinary missing key, which is the other check's job.

The first version found only one of the two real cases, because the other
declares its translator through a wrapper that takes no argument and carries
the namespace one indirection away. Wrappers are now resolved from source, and
both historical defects are reproduced from the code as it stood before each
fix, with the qualified key named in the message.

Self-test now makes twenty-eight assertions, including a planted wrapper.
2026-09-28 12:04:36 +00:00
Doxa Maps
ac2556cc8a Hardcoded-English gate: three agents' triage of its first run
gospel, upg100 and exp-shepherd triaged 300-odd hits between them, and most of
what they found was the gate being wrong rather than the code.

False positives removed, each from a real hit: class and id suffixes built by
interpolation, relative URLs, custom-element tag names, and empty matches.
Developer strings are now exempt by CALL SITE rather than by content, because
the site is the fact and the wording is not. A split template literal is one
finding reported as its whole string with its holes marked, instead of one hit
per fragment -- that alone was turning single defects into three.

Text inside a dir="ltr" island is DELIBERATE English, the marker these plugins
already use for content ruled to stay English. It is listed but not counted, so
nobody is asked to undo a decision.

The header gains a second blind spot, found the same day by exp-shepherd: a
guarded fallback is judged by its shape, not by whether the lookup it guards
works. A key read with the wrong prefix misses every time, making the English
arm the normal path in every locale while this stays quiet. I had triaged
exactly that line as "not a leak" because the key existed -- existing and being
found are different things.

Self-test is twenty-three assertions: every leak shape seen, every correct form
silent, both blind spots pinned.
2026-09-28 12:04:36 +00:00
Doxa Maps
11e2d7eaaf Hardcoded-English gate reads interpolations and two more fallback idioms
Interpolation expressions carry the same defect as bound attributes: a literal
the reader sees, assembled in an expression a template walker skips. Added
before the gate lands, so the count each owner triages is the real one. It rose
by twenty-nine.

Two more shapes are fallbacks rather than leaks, both found by triaging real
hits. A proxy translator whose member read misses, and the idiom that asks
whether the translator handed the key back, which is what a missing key looks
like in vue-i18n. The second is matched on the key's shape -- dotted, no spaces
-- so an ordinary comparison against a word is not mistaken for one.

Both directions are planted in the self-test, which now makes seventeen
assertions: every leak shape must be seen, and every correct form must be
silent.
2026-09-28 12:04:36 +00:00
Doxa Maps
b51a8a6402 One hardcoded-English gate instead of four private scanners
sr, upg100, gospel and regions each wrote a scanner for user-visible English
that never reaches a translator, and each found real defects. Four copies means
four sets of blind spots and four allow-lists drifting apart, so this merges
them. Every rule earns its place from a defect one of those scanners caught.

Templates and script blocks are parsed, never matched with a regex: a regex for
string literals matches from the closing quote of one to the opening quote of
the next, which produced 137 phantom findings before this was parsed properly.
Translator aliases are read out of each file, because `const t2 = t` defeats a
check that knows only the base names. Guarded fallbacks are not leaks, in both
shapes that exist here: an existence check around the call, and a proxy whose
member read misses. Bound attributes are parsed too, in either quote style,
which is the blind spot every one of the four scanners shared.

The self-test plants a leak of each kind and requires the gate to see it, then
plants the correct forms and requires silence. It also asserts the fixtures are
valid Vue first, because a malformed fixture and a blind scanner produce the
same silence.

The header records what this cannot see: single-word strings. A bare 'Other'
used as a fallback label is user-visible English and will not be reported. That
trade keeps the signal readable, and the self-test pins it so that closing the
gap forces the header to be corrected rather than left stale.
2026-09-28 12:04:36 +00:00
Doxa Maps
3d2c16a707 check:facet-shape tests the facet gate's injected helper headlessly and runs in gates:pre
Some checks are pending
Deploy to GitHub Pages / build (push) Waiting to run
Deploy to GitHub Pages / deploy (push) Blocked by required conditions
The WALK string the facet gate injects into the page moves to a browser-free
module, internal/lib/facet-walk.mjs; the gate imports it, and
internal/check-facet-shape.mjs imports the same string and evaluates it on both
engines' source shapes without a browser. check:facet-shape ran from the browser
gate's own file, so the runner routed it to the browser slot; now it is headless
and joins gates:pre. Poisoned (the { geojson } unwrap disabled) it reports 4/5.
2026-09-28 10:27:43 +00:00
Doxa Maps
a98cbe8055 check:facet-changes-map — measure MapLibre too, not just Mapbox
BUG-FACET-GATE-CANNOT-MEASURE-MAPLIBRE. The gate read source._data four times,
and the two engines store it differently -- cited from the bundles this repo
loads, the same constructor in each:

  mapbox-gl v3.24.0   this._data = i.data                          the raw GeoJSON
  maplibre-gl         this._data = "string"==typeof i.data
                                   ? {url:…} : {geojson: i.data}

A GATE blind to MapLibre passes a map that is broken there, which is worse than
a plugin carrying the same bug.

★ ALL FOUR READS WERE BROKEN, NOT THREE. The census called :145 "guarded" because
it had `s._data || (s.serialize && s.serialize().data)` -- but `||` fires only
when the left side is FALSY, and {geojson: …} is TRUTHY, so the fallback never
ran. The wrapper has to be UNWRAPPED, not fallen back from. That correction is
the reason this is a shared helper rather than a copied one-liner.

__srcFeatures() goes in the WALK bundle -- the in-page script the gate already
injects -- NOT as an import of library/maps/pin-selection.js: all four call sites
run inside page.evaluate / page.waitForFunction bodies, which are serialised to
the browser, where a Node-side import does not exist. Same logic as
readSourceFeatures(), duplicated across a process boundary, which is the one
place duplication is not a smell.

A remote (url) source returns null, not 0: null means "could not measure", while
0 would mean "the map drew nothing", which is the finding this gate reports.

THE MAPLIBRE SKIP IS RETIRED. It exited 0 while announcing SKIP -- NOT MEASURED,
correct for the limitation but a gate that never looked at half the product. A
source present with an unreadable payload is now a real failure with a
diagnostic, because the known cause of that state is fixed.

check:facet-shape negative-tests the helper headlessly against both engine
shapes, in sr's check-pin-selection style, evaluating it OUT OF THE WALK STRING
so the thing tested is the thing injected. 5/5. Poison-tested: with the unwrap
removed, ONLY the maplibre case fails (4/5), so it catches the regression and is
not indiscriminate.

internal/ is the build owner's under rule 5B -- source sha to studio. Running the
gate itself launches a browser and is rega's.
2026-09-28 09:48:45 +00:00
Doxa Maps
a9dfe29200 Close the browser guard's path-shaped bypass: check-experimental-shelf imported playwright by file path
check-experimental-shelf imported resolve(ROOT, 'node_modules/playwright/index.mjs')
dynamically, named no package specifier, and so launched Chromium outside the
slot while check:browser-gates-guarded passed it (upg100 found and measured it).
It now imports the guard, and an unslotted run exits 75. The check also fails a
path to node_modules/playwright (or playwright-core) anywhere in code; with the
old import restored it fails that file.
2026-09-28 09:07:30 +00:00
Doxa Maps
5a36f17467 check:packets-all — exit 75 is NOT MEASURED, not FAIL
Landing 48's browser guard made check-packet-render refuse to launch outside the
browser slot and exit 75. This gate read `ok = res.status === 0` -- two outcomes,
not three -- so it reported "5 packet(s) FAILED, 0 passed" about five packets it
had never opened, naming five innocent files. Someone would have gone looking for
a rendering bug that does not exist.

run-parallel.mjs:262 states the contract: 75 (EX_TEMPFAIL) means the gate COULD
NOT LOOK, which is neither a pass nor a failure. Unmeasured packets now print ⊘
and are excluded from the pass/fail count, with the count of them in the summary.

AND THE CASE THAT MATTERS MORE: zero failures and zero looks are the same
`failed.length === 0`. Without a guard, a run that could not open a single packet
prints "PASS -- every generated packet renders" -- the most flattering possible
lie about a measurement nobody took, and the same shape as the stale-artifact bug
this file was fixed for earlier today. When nothing was measured it now exits 75
itself, naming the command that runs it in the slot, so the runner buckets the
whole gate NOT MEASURED rather than green.

Verified: outside the slot the gate now exits 75 and says so, instead of exiting
1 with five false failures.
2026-09-28 09:07:29 +00:00
Doxa Maps
cdcdd84cf9 verify:browser-all reads a gate's exit 75 as NOT MEASURED, not as a failure
It counted any non-zero exit as FAIL, so a gate that could not measure (no
browser slot, a server mid-build) read as a broken product, the same bug
upg100 found in check:packets-all once the browser guard made gates refuse
outside the slot. Exit 75 is now NOT MEASURED, shown as such, and a run with
no failures but any unmeasured gate exits 75 itself (INCOMPLETE), never green.
2026-09-28 09:07:29 +00:00
Doxa Maps
f44da45258 A test for the source reader, because a no-op cannot be seen any other way
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.
2026-09-28 08:31:19 +00:00
Doxa Maps
1223fa7b45 check:studio-rail — scope the label rule by PROFILE stage, not bundle stage
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.
2026-09-28 08:31:18 +00:00
Doxa Maps
114df6f6c4 check:studio-rail — a fixture self-test for the declared-label rule
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.
2026-09-28 08:31:18 +00:00
Doxa Maps
112f4cee77 check:studio-rail — a production profile must DECLARE its name
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.
2026-09-28 08:31:18 +00:00
Doxa Maps
ba131ca4a7 check:browser-gates-guarded's comment no longer spells a literal playwright call
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).
2026-09-28 08:20:21 +00:00
Doxa Maps
79296f87fb A fresh clone's first bun run dev line says Rudder is optional and needs Go 1.22+
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.
2026-09-28 08:20:21 +00:00
Doxa Maps
22a1561d2f check:browser-gates-guarded matches a real playwright import, so no detector needs a whole-file exclusion
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.
2026-09-28 08:20:21 +00:00
Doxa Maps
462f553442 check:browser-gates-guarded names its detector files by path
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.
2026-09-28 08:20:21 +00:00
Doxa Maps
78eb6bdcb8 check:packets-all — skip stale artifacts, and follow the series on the funnel
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.
2026-09-28 08:20:21 +00:00
Doxa Maps
fbc517c94a A browser gate refuses to launch outside the browser slot; check:browser-gates-guarded keeps it that way
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.
2026-09-28 08:20:21 +00:00
Doxa Maps
35955a4db6 check:one-palette sees a bundle's private src/colors/ palettes; Rudder's offline hint states Go 1.22+ and the setup
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.
2026-09-28 07:07:33 +00:00
Doxa Maps
6451a2b158 bun run desktop:build <plugin> packages a desktop plugin as an app; check:targets keeps non-web plugins out of the web build
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.
2026-09-28 06:44:14 +00:00
Doxa Maps
e53df81cb5 check:poster-action-print also pins PosterDialog's print default and its download opt-ins 2026-09-28 05:30:58 +00:00
Doxa Maps
11129455e4 posters: SERIES is the one switch; header wording joins it; country mode fixed
Some checks are pending
Deploy to GitHub Pages / build (push) Waiting to run
Deploy to GitHub Pages / deploy (push) Blocked by required conditions
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.
2026-09-28 04:57:44 +00:00
Doxa Maps
6778a0828f A poster button opens the print dialog by default; downloading is an explicit, explained opt-in
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.
2026-09-28 04:41:00 +00:00
Doxa Maps
12d7f218f3 posters: a SERIES parameter, so a country poster stops rendering the UPG-100 packet
Some checks are pending
Deploy to GitHub Pages / build (push) Waiting to run
Deploy to GitHub Pages / deploy (push) Blocked by required conditions
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.
2026-09-28 04:19:27 +00:00
Doxa Maps
534bbdd5af Rudder stays out of the Research Gallery and makes no network call on a static host
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.
2026-09-28 03:12:46 +00:00
Doxa Maps
daa6ee6eda Delete the "Show on map does not select the zone" exemption — it outlived its defect
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.
2026-09-28 03:12:45 +00:00
Doxa Maps
2e92a05b30 A preset keeps its profile's defaults: instance props merge over the meta config instead of replacing it
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.
2026-09-28 03:12:45 +00:00
Doxa Maps
35f682f7cb The 32 are scattered through the life now, not stacked into a bar
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.
2026-09-28 02:38:50 +00:00
Doxa Maps
085394fff2 public-hygiene: the packets' prayer point 1 may say persecution, exactly as written; nothing else may
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.
2026-09-28 02:38:49 +00:00
Doxa Maps
64a2f42730 The dev server serves a bundle's script embed (./<b>/<b>.js) as the built tree does
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.
2026-09-28 02:38:49 +00:00
Doxa Maps
cdb9efc9d4 Plugin Studio names each row from the plugin's own meta label, never a bare profile id
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.
2026-09-28 02:38:49 +00:00
Doxa Maps
dfb3dabe55 Merge remote-tracking branch 'origin/main' into bundler-1/work
Some checks are pending
Deploy to GitHub Pages / build (push) Waiting to run
Deploy to GitHub Pages / deploy (push) Blocked by required conditions
2026-09-28 02:16:49 +00:00
Doxa Maps
ed87323592 Big Picture speaks Arabic where it was still speaking English: chrome and data labels
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.
2026-09-28 02:01:46 +00:00
Doxa Maps
2a692021c0 Boundary attribution travels with the boundaries
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.
2026-09-28 01:55:03 +00:00
Doxa Maps
ee868aaa92 Boundary attribution travels with the boundaries
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.
2026-09-28 01:42:41 +00:00
Doxa Maps
6853b822bb Merge remote-tracking branch 'origin/main' into bundler-1/work 2026-09-28 01:38:54 +00:00
Doxa Maps
e8ddfc34a6 Vue and Tabulator ship their MIT notices, and a third-party register
Some checks are pending
Deploy to GitHub Pages / build (push) Waiting to run
Deploy to GitHub Pages / deploy (push) Blocked by required conditions
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.
2026-09-28 01:30:47 +00:00
Doxa Maps
488ac03bef Merge remote-tracking branch 'origin/main' into bundler-1/work 2026-09-28 01:14:00 +00:00
Doxa Maps
170febe2de Strip CSS comments from every shadow-style literal shape, not only useShadowStyles(…)
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.
2026-09-28 00:56:58 +00:00
Doxa Maps
dca4601568 Merge remote-tracking branch 'origin/main' into bundler-1/work 2026-09-28 00:29:52 +00:00
Doxa Maps
a08cf1e4d8 Gate: no PocketBase hooks in the repo
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.
2026-09-28 00:20:34 +00:00
Doxa Maps
a37a4e9c12 The LOCALE column clears for Big Picture: the held numerals decision is allowed at its one site
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.
2026-09-28 00:20:33 +00:00
Doxa Maps
82e05018ee composes speaks three vocabularies, and unresolved refs now fail
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.
2026-09-28 00:20:33 +00:00
Doxa Maps
b919371b26 Follow a remote: DX_SYNC_REMOTE also takes a repo URL, and checks every 5 s by default
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.
2026-09-27 23:54:30 +00:00
Doxa Maps
7b49aef938 bun run dev runs PocketBase at /api/pb, or uses a shared one
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.
2026-09-27 23:54:30 +00:00
Doxa Maps
de004b3703 bun run dev can follow a shared branch: fast-forward only, never over local edits
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.
2026-09-27 23:54:30 +00:00
Doxa Maps
bac8a1048c Rudder with RUDDER_ENGINE=0 never starts the engine, and a page on another machine never offers to record the server's mic
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.
2026-09-27 23:24:08 +00:00
Doxa Maps
9b150b902d Square panel: no child brings its own outline
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.
2026-09-27 23:23:42 +00:00