Last tick fixed 15 of 26 profiles that shipped with an empty description.
Without a gate that fix has a half-life: the north star is "develop a plugin
and see it live", so new plugins arrive often and each one is a fresh chance
to regress this silently. The repo already validates @element, but only
against a MALFORMED value — an ABSENT @description fails quietly, the
generator writes an empty string, every gate passes, and the defect only
surfaces to a human who opens the studio and reads.
Reads the BUILT manifest, not the .vue sources, on purpose. Re-deriving the
description here would mean reimplementing generate-tree's TAG_RE — which is
exactly what hid two of the fifteen last tick, when a hand-written copy of
that regex reported success while the real rule extracted nothing. Check the
artifact the consumer reads, never a reimplementation of how it was produced.
MIN_CHARS is calibrated against the real corpus, not guessed: shortest
genuine description 62 chars, median 81, longest 192, so the 25-char floor
sits clear of every legitimate entry while still catching a stub.
Negative-tested four ways, including end to end: deleting a @description tag
from a .vue source FAILS THE WHOLE BUILD (exit 1) naming the profile and the
file to edit; restoring it returns the build to green. Also fails on a short
stub and on a near-empty manifest (the vacuous-pass guard), rather than
passing for lack of anything to check.
Driver 2026-09-21: 'a folder called deployment which will store the capacitor
folder, wordpress folder and tauri folder', and 'a folder called documentation
to store the contributing folder so docs are in one place'.
docs/ + contributing/ + CONTRIBUTING.md -> documentation/ (three doc homes into
one). capacitor/ + wordpress/ + src-tauri/ -> deployment/.
The public/ -> dist/ rename the Driver asked about is NOT here: after seeing the
measured blast radius (855 tracked files, 54 source refs, and public/ is the
served web-root four consumers point at) the Driver withdrew it - 'keep the
public folder the same'.
Path references were rewritten mechanically (258 across 87 files), but three
classes needed hand fixes because a prefix regex cannot see them:
- join(ROOT,'docs',...) in 10 internal scripts - a quoted SEGMENT, not a prefix
- tauri frontendDist '../public' -> '../../public', since src-tauri is now one
level deeper; check:tauri caught this exactly as the card predicted
- the WordPress contract test's dirname(__DIR__,2) -> 3, same reason
Also reverted one over-reach: the rewrite renamed a FINDING-CATEGORY label
'docs' in public-hygiene-check.mjs, which is a rule name in its own header, not
a path.
Verified: full build ALL GUARDS PASS, SRI consistent; check:tauri, check:mobile
(cap sync + webDir OK), check:wp-embed, quoted-scripts, capability-citations,
capability-exemplars, library-index, public-hygiene, parse and drop all pass.
check:catalog-identity hashes each live bundle against the CATALOG'S OWN
manifest - deliberately, per its header. That proves the served tree is
internally consistent; it can never prove it is current. A catalog three
publishes behind is perfectly self-consistent and passes clean.
Measured today: after baking the Mapbox token, catalog-identity reported '17 of
17 identity-proven - PASS' while every live leaf still served BAKED_TOKEN = ''
and every map on the platform was broken.
The two gates answer different questions and neither subsumes the other:
identity is the live catalog consistent WITH ITSELF? (tamper/corruption)
currency is the live catalog the SAME BUILD as ours? (staleness)
It checks BOTH bundles and leaf pages, because a bundle-only version of this
gate also passed: baking the token changed 37 leaf HTML pages and ZERO .js
bundles. generate-tree owns the leaves, vite owns the bundles, and a publish can
change either independently.
Current reading: bundles 17 current, leaves 17 STALE.
Negative-tested: unreachable catalog SKIPs (offline must not fail a build), a
manifest yielding 0 integrity values FAILs rather than passing vacuously, and a
missing local build SKIPs.
tauri build produced target/release/doxa-maps-desktop (65 MB ELF) and a 58 MB
.deb (Package: doxa-maps, Depends: libwebkit2gtk-4.1-0 libgtk-3-0). tauri info
reports the environment fully green and correctly detects frontendDist
../public, Vue.js, Vite.
The web tree is compiled INTO the binary rather than shipped as files, which is
why the .deb holds only 6 entries - confirmed by 19 distinct embedded
doxa-maps/... asset paths in the ELF and by size (a logic-free shell is 65 MB
only because the 132 MB public/ tree is compressed inside it).
bundle.targets narrowed from all to deb: rpmbuild and linuxdeploy are absent
here, so all would fail two of three formats at the END of a ~6-minute release
build.
The CLI is a devDependency (@tauri-apps/cli, prebuilt binary) rather than
cargo install tauri-cli, which compiles from source.
Status stays BUILT, NOT SHOT. The GUI launch is unverified because this box is
headless - the binary panics in tao with 'Failed to initialize GTK', the
absence of a display rather than a defect, and no Xvfb is installed.
Method 5 was the last aspirational entry in the 5-deploy-method model: 0
tauri.conf.json, 0 Cargo.toml, 0 .rs. It is now scaffolded against the v2
config schema, mirroring the Capacitor pattern that already works.
frontendDist points at the SAME public/ tree Capacitor and the CDN serve, so
one build still feeds every consumer - now four instead of three. The crate
carries no application logic on purpose: the product is the web tree, this is
a window around it.
check:tauri is the analogue of cap:verify and exists for the same reason
capacitor.config.json warns about in capitals: a wrong frontendDist or a window
url that does not exist launches a BLANK WHITE WINDOW, not an error. It runs
with no Rust toolchain and no Tauri CLI, so CI can run it. Negative-tested five
ways - wrong frontendDist, missing window url, identifier drift from the
Capacitor appId, empty windows array, missing crate file - each exits 1, and
the config was restored byte-identical after every test.
Status stays BUILT, NOT SHOT. rustc parses main.rs (it reaches line 14 and
fails only on the uninstalled tauri crate, a dependency error not a syntax
one), but producing a binary needs the Tauri CLI plus system webview libs.
Also recorded the token/CORS caveat up front: a Tauri webview serves from
tauri://localhost, so the token endpoint must send CORS for that origin -
the same surprise Capacitor documents for https://localhost.
Last tick's fix covered the shared map-control base. Sweeping the rendered DOM of all 17
bundles found three more unnamed buttons — the same "Collapse legend" control in three
map bundles, again from one shared component. Its only content is an svg caret, so with
title alone its accessible name was empty.
It is a disclosure control, so it now reports aria-expanded as well. Verified by toggling
it: the label flips Collapse/Expand and aria-expanded flips true/false.
The gate measures the rendered DOM through shadow roots, because a source grep cannot
answer this question. There are 132 <button> hits here and most have visible text, which
IS a valid accessible name; aria-label is often on a following line. Only the DOM knows
which buttons ended up nameless. It counts aria-label, a resolved aria-labelledby, or
visible text, and deliberately does not count title, which is the defect it was written
for. Hidden buttons are skipped since assistive tech cannot reach them either.
The sweep went 319/322 before the legend fix to 322/322 after, which is the evidence it
detects real defects rather than passing vacuously.
Card 1 asks that one public Forgejo repo be the live catalog source and that the merge
lose nothing. Both halves were marked done-verified on 2026-09-19, two publishes ago, so
this re-measures rather than trusting the field: the catalog is still a zero-diff match
for the freshly-built tree, all 17 live bundles hash to their own manifest integrity, and
the live public manifest is byte-identical to the catalog's.
An HTTP status cannot make that claim. /app/doxa-maps/manifest.json returns 200 and is a
2,872-byte HTML sign-in page, so a status check reads the login wall as healthy. Every
profile already carries an integrity hash the build computed for an unrelated reason, so
comparing it against the live bytes is a cryptographic identity proof with no new source
of truth to keep in sync.
Network-dependent, so not in build:unlocked and not in CI. It distinguishes "could not
reach" from "reached and wrong" and only the latter fails, because a gate that fails on a
network blip gets switched off.
Negative-tested four ways: the login wall fails with the reason, an unreachable host
skips at exit 0, the catalog-comparison mode confirms the sha256 match, and the baseline
proves 17 of 17.
"Which bundle is the capability-N example?" has been answered wrongly in three separate
artifacts, each fixed in isolation while the others kept the bug: verify-5-capabilities
cited doxa-vision-card as cap-1, the README made the identical claim, and the
/plugin-designer skill still cites both that and unified-research-dashboard, a bundle
that does not exist.
The skill cannot be gated from this repo. It lives in the session config directory, in
no git repo, so a checker pointed at it would pass here and skip silently everywhere
else — the gate-that-cannot-fail shape. So instead of policing the skill, give the repo
an authoritative generated answer that prose can cite and a gate can verify.
The table derives from manifest.json, which the build computed from each folder's real
shape, and reference.md now points at it with the reason: a folder's apparent simplicity
does not decide its tier. doxa-vision-card looks like the lightest thing in the repo and
is capability 2, because it ships an instances file.
check:capability-exemplars regenerates and content-compares rather than checking mtimes,
because a full build rewrites manifest.json every time and a gate that cries wolf after
every build gets switched off.
The CAP 1 slot shot doxa-vision-card, which is not a single element: it ships
doxa-vision-card.instances.json with two instances, so discover.mjs types it
parameterized and manifest.json records "parameterized" — capability 2.
The same error had already been found and fixed in verify-5-capabilities.mjs, whose own
comment records that it had been sampling cap-2 twice and cap-1 never. That fix landed in
the gate and never reached the README: two artifacts encoding one fact, with nothing
forcing agreement. Fixing one copy was not fixing the fact.
all-people-groups is a genuine cap-1 — one .vue, no instances file, manifest type single.
The README now also says what decides the tier, since the folder's apparent simplicity is
the trap: doxa-vision-card looks like the lightest thing in the repo.
check:capability-citations compares every README capability claim against the manifest,
blocking in CI. Its first matcher used \Z, which is not JavaScript, so it matched zero
sections while still reporting a citation count — a gate reporting a number it had not
verified. It now splits on headings and the count reflects real matches.
The badge read "27/26 zones" — a count exceeding its own total — in two shipped README
screenshots. 26 was the leaf count before slice 09b-6 added the Frontier-Asia uncovered
rows; the literal was never updated, so it drifted the moment the data grew. The
denominator is now derived from perZone.size, which tracks the zone list automatically
and cannot disagree again. Measured from clusters.json: 13 regional + 12 asia + 2
uncovered + 1 deaf = 28. The corrected badge also surfaces 1 genuinely unresolved zone
that the stale denominator was masking.
legend-tree.js asserted "33 leaves" and "7 Frontier-Asia uncovered" in a comment; the
shipped data carries 28 and 2. Corrected to point at the arrays instead of a count.
README showed "List — page 1" and "List — page 2" as the same picture. The shot loaded
one URL twice and tried to reach a page 2 by scrolling and clicking Next; neither exists,
the list has no pagination, and every attempt sat in try/catch so the miss was silent.
The PNGs differed by 0.16% render noise, so no checksum would have caught them. Mean-
while poster-wall — a real manifest-listed profile — had no screenshot at all. Shoot the
real third thing rather than fake a fourth.
check:shot-distinctness gates this, blocking in CI. It keys on url+viewport, so the
deploy2 mobile shot of the same SPA stays legitimate rather than a false positive.
README drew the two cap-5 products apart in prose and even warned that captioning a
studio shot as the product SPA is a documented mistake — then filled its plugin-studio
slot with cap5-doxa-maps-spa.png. Both cap-5 figures showed the same app and no
plugin-studio screenshot existed anywhere in the repo.
A slot filled with the wrong real image looks exactly like a correct one; nothing 404s.
So the fix is to shoot the missing artifact, not re-caption around the gap. Both cap-5
slots now use generated, freshness-gated images.
Also adds check:shot-fidelity. The shot run reported 13x OK while every emoji rendered
as a tofu box: this machine has no emoji font. Measured rather than assumed — the DOM
carries 27 correct codepoints, so the markup is right and the defect belongs to the shot
machine. Reports by default because installing a font is a Driver call; --strict blocks
once one is present.
README-SECTION-3 reproduces the scripts block verbatim and carries its own plea to
re-transcribe after every package.json edit. It drifted anyway, twice: cap:sync still
showed the bunx form that does not exist here, and build:unlocked was missing
check:public-hygiene and check:label-joins.
The second is the dangerous shape. An omission in a quoted list has no wrong character
to notice, so a reader auditing what the build verifies concludes the secret scan is not
in the chain. The warning was already there and did not work, because re-transcribing by
hand is what gets skipped when the edit felt unrelated.
Blocking in CI: it diffs two files already in the checkout, so a failure is always real.
Negative-tested both ways, including a matcher that matches too little failing rather
than passing vacuously.
- internal/playground → internal/plugin-gallery; playground-smoke → plugin-gallery-smoke;
generate-spa → generate-gallery (scripts: gallery, smoke:gallery; smoke:playground stays as alias)
- generate-gallery exports renderGallery(model, { mode }) — the dev plugin serves the same page
at / from the discovered registry (dev mode imports the dev entries, ladder popovers on);
the build writes public/index.html from the manifest (byte-identical to before)
- Docs and probes use the gallery vocabulary
- index.html is generated by internal/generate-spa.mjs from doxa-maps/manifest.json: one
self-contained page that lists every bundle, profile and instance, mounts the chosen one,
and offers Open page / Copy embed; build ends with `bun run spa` (the material shell is no
longer installed; nav.js, icons.js, languages.js, pane-wrapper/, external/ and _headers
leave public/)
- The website drop-in follows the same contract: dropin.include in the build config,
sync-dropin --from-config --prune, public-hygiene --strict fails on any other root entry
- Production bundles no longer carry CSS comments: a build-only Vite plugin strips them from
every shadow-style block (about 140 per map bundle, 5% smaller); remaining comments reworded
- Share pages and the pin renderer drop their note-style comments
- SemanticTreeLegend: the UUPGs/Population columns were a fixed 44px, sized for the old
9px header / 10.5px numbers; at the desktop scale "POPULATION" collided with "UUPGs"
and "139.0M" spilled into the neighbouring column. Base column is now 60px, desktop
76px (header 10px, numbers 13.5px, no wrapping)
- The panel follows --map-legend-width like the detail card and the geocoder pill; the
research map's wide-host legend is 400px so "Full Prayer Coverage" stays on one line
- New probe: `bun run probe:legend-columns` measures cell boxes vs real text extents and
fails on any spill or overlap (desktop and phone sheet)
- ShareButton resolves its link from where the bundle is hosted (profile-config.shareUrl ->
embedBaseUrl -> window.MAP_EMBED_BASE_URL -> the bundle's own script URL); profiles pass
tree-relative embed pages, so links work in the playground, on doxa.life and on any CDN
- Generated leaf pages try token endpoints in order (override -> configured absolute URL ->
same-origin) and only then offer the paste-once box; build config points at the canonical
https://doxa.life/api/maps/token
- Dev playground extracted from vite.config.js into internal/playground/{discover,render,dev-plugin}.mjs;
serves /assets/* like the built site so data-driven maps initialise; passes assetsBase +
shareUrl to every element; header ladder with a Start-here primer and the five kinds of
mounting files (single-element, preset-element, multi-view-element, composite-element, app-shell)
- New gates: smoke:playground (every element boots, with status text + data diagnostics),
smoke:share (Share URL resolves and reopens the same map), sync:dropin (mirror public/ into a
host checkout, never credentials), deploy:dev (build + sync)
- MapToolbar + ShareButton on every map profile; UPG-100 composite (map + prayer lists) on the
pane layout; shared --map-radius token consumed by the legend components
- docs/reference.md: profile types table, profile-config assetsBase/shareUrl/embedBaseUrl, gates
New permanent harness (bun run smoke:embeds): loads every profile page from the
partner-facing manifest headless (SwiftShader software WebGL — no GPU needed),
asserts the custom element defines + populates its shadow DOM, no ProfileLoader
error card, and a WebGL canvas on map profiles. First full run: 17/17 pass —
8 map profiles paint a canvas, 9 DOM profiles (lists/tables/charts/walls)
populate. Exit 1 on any failure, so CI can gate on it.
Standalone repo that builds embeddable JAMstack map bundles (web components),
composes them into a lazy-loaded SPA via layouts/, and optionally wraps a
Capacitor Android/iOS app — one build, choose your output/CDN. Diátaxis docs.
History reset to a single clean commit; no PII, IPs, or internal terms.