106 commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
6732c8d430 |
Guard the packet catalog: the 12 prayer points and the wheel had no parity gate
check:i18n-parity covers library/i18n/locales — the APP catalogs. Nothing covered library/scripts/posters/packet-strings.json, which is a separate file with its own shape and is where the 12 PRAYER POINTS and the PRAYER WHEEL live. SIX gates read it and not one checked that a language block is complete. All ten locales happened to be healthy — 19/21 string keys, 12 points with every field, 12 rhythms — but nothing held them there. A point dropped from one block renders that slot through the per-key English fallback: correct runtime behaviour, invisible defect, on a page the reader is meant to pray through. ★ IT ASSERTS PRESENCE, NOT TRANSLATEDNESS, and that is the load-bearing choice. check-packet-render printed "19 strings translated" and only ever asserted `!== 0`, so a locale regressing from 19 to 1 passed silently. The fix is NOT to pin 19: measured, de translates 18 of the same 19 keys and fr 17, because a word or two legitimately matches English — a hardcoded count fails two CORRECT catalogs. So parity asserts every English key EXISTS in every block, a structural fact no translation choice can change, and the render gate gains a FLOOR of two-thirds derived from the catalog itself. point4_passage + point4_passage_credit are DECLARED-ABSENT with the reason on record (Mark 5:1-20, public-domain licensing per language). Naming the exception beats tolerating it silently: the day a licensed translation lands, deleting the entry tightens the gate by itself. MUTATION-TESTED, five defect classes, each caught with a precise message: a dropped string key, an 11th-of-12 prayer point, an EMPTY point text, a missing wheel rhythm, and a block reverted wholesale to English. The floor was tested separately by reverting de to 3 translated of 19 — "only 3 differ from English (floor 13)". ★ THE TWO GATES ANSWER DIFFERENT QUESTIONS, and a partial language shows both at once. Swahili at 3 of 21 keys, no points, no rhythms: check-packet-render PASSES — a partial translation renders, Swahili where translated, English where not, zero blank slots — while check:packet-strings-parity FAILS and names exactly what is missing, including that the points and wheel would fall back wholesale. Before this, only the first question had a gate, so a half-finished block was indistinguishable from a complete one at build time. gates:post is now 22 steps; 11/11 packets still pass the sweep. |
||
|
|
e5fe33b230 |
prayer-walk-planner can be shared — the last published map with no sharing path
check:share-policy's known-debt line is now GONE: it named two published maps, then
one, now zero. Every published map has exactly one decided sharing path.
★ THE OPEN QUESTION WAS ANSWERED BY THE CODE, NOT BY A RULING. This was held back
asking "is it MEANT to be shareable? a link carrying no route arrives as an empty
planner, and a button that reliably produces a blank map is worse than none". That
framing assumed a design choice existed. It did not: the profile keeps polygons in
localStorage and has NO BACKEND — its own header says so — so a URL cannot carry a
drawn route under any design. The shareable artefact is the PLANNER, exactly as
every other map shares its own page. The failure only exists if the link CLAIMS to
carry the route, so the status strip says once, on click: "This link shares the
planner. Your drawn zones stay on this device." — using the status mechanism the
profile already had rather than adding a surface.
★ ADDING <ShareButton> WAS NOT ENOUGH, AND check:share-policy COULD NOT SEE IT.
The button rendered NOWHERE. This bundle mounts its SFC directly as the custom
element — deliberate, it is what surfaces the defineExpose()d client-side API on the
DOM element ("the only API we can build has to be CLIENT SIDE") — and so installed
no i18n. ShareButton calls useI18n(), which throws in setup(), and index.js's own
header records the cost: "a thrown setup() removes the whole shadow-DOM subtree
silently." Measured: a runtime SyntaxError, five toolbar buttons rendered, the sixth
absent — while the policy gate, which counts <ShareButton> in SOURCE, said 1 button
and PASSED. That blind spot is now written into the gate, pointing at the smoke that
does catch it.
Fixed WITHOUT changing the mounting shape: defineCustomElement takes configureApp
whichever component is the root, so i18n installs and the API stays on the element.
The direct-mount decision and i18n were orthogonal; I had assumed they were coupled.
VERIFIED AT THE RENDERED LAYER: share-roundtrip-smoke resolves the url 200 text/html
and REOPENS it as <prayer-walk-planner> with a rendered canvas. The client-side API
was re-verified after the change — all 7 exposed methods present, addPolygon ->
listPolygons round-trips, 0 page errors. i18n verified in 6 locales: Share /
Compartir / مشاركة / साझा करें / 分享 / Partager. Added to smoke:share-all, now
12/12 pass 0 fail.
|
||
|
|
3a468c2bc3 |
The poster print preview rasterises too, and a deployment can ship sources without rasters
Some checks are pending
build-gate / build (push) Waiting to run
posters/*.webp is not shipped by default (42MB; bundling pushed the tree to 80MB,
git-packed to 57MiB, and the catalog rejected the push). The DOWNLOAD path has
handled that since
|
||
|
|
e5c2c77561 |
all-people-groups can be shared: its own button, verified by a round-trip
Some checks are pending
build-gate / build (push) Waiting to run
It was published, embeddable and reachable with NO sharing path by any mechanism — no ShareButton, no shareUrl config, no composite parent overriding one on its behalf — while every other published map had exactly one. A reader who landed on it could not send it to anyone, which is the point of an embeddable jamstack map. check:share-policy's known debt falls from 2 published maps to 1. The button is in a MapToolbar (pure layout shell: position, z-index, mobile offsets) rather than hand-positioned, so it sits where every other map's controls sit. has-geocoder=false because this profile mounts no search pill. Only the share control is added — zoom/location/fullscreen each need a live mapRef this bundle does not expose, and the card asks for a sharing path, not a toolbar. NOT gated on appReady, unlike this bundle's legend. Sharing a map does not require its data to have loaded, and gating it would remove the affordance exactly when a slow or failed load makes a reader most likely to want the link. ★ WHY A RENDERED CHECK WAS NEEDED. check:share-policy counts <ShareButton> in SOURCE. Mutation-tested by gating the toolbar off: the policy gate still read "1 button / own-button" and stayed GREEN while the button was absent from the DOM. A source count cannot tell a rendered button from an unrendered one. So this is verified by the EXISTING share-roundtrip-smoke, not a new gate. I wrote one, then deleted it on finding that smoke does strictly more — it fetches the URL and REOPENS it, asserting the same custom element renders, which catches SPA fallbacks and hash-stripping that a link-text check cannot. A second share checker would be the fourth-print-path mistake this repo has already paid for once. Result: share URL resolves 200 text/html and reopens as <all-people-groups> with a rendered canvas. i18n verified at the rendered layer: under ?lang= the button title reads Share / Compartir / مشاركة / साझा करें / 分享, so the new affordance is translated in every locale, RTL included. buttons.share is one of the keys restored to parity earlier. ALSO FIXED, found by running the suite: smoke:share-all was testing the LEAF urls of the two 'header'-policy bundles, whose button lives in the MFE shell by design, so it reported 10/12 with two failures that were the policy working as intended. Swapped country-posters/countries-map for country-posters-mfe and dropped the stale my-upg-100-list leaf (upg-100-mfe was already listed). Now 11/11 pass, 0 fail. check-share-policy records the coupling so the list cannot drift from the policy again. prayer-walk-planner stays debt-no-share, deliberately. It builds a personal walking route from polygons the reader draws; a link carrying none of that arrives as an empty planner, and a button that reliably produces a blank map is worse than none because it looks like sharing works. That is a semantics decision, not an implementation one. |
||
|
|
e952e01e4c |
Assert the packet numbers that were only being printed, and run the gate on every locale
check-packet-render's own header states the law it exists for — "never print a number
without asserting on it" — and two of the nine numbers it printed were decoration.
PAGE SIZE. Printed "612 x 792 pt", never compared. The landscape check only catches
h <= w, so a wrong PORTRAIT size sailed through: A4 is 595x842, taller-than-wide like
Letter, and would have passed on a packet printing with wrong margins on every US
printer. Not hypothetical — @page{size:...} silently falls back in Chromium, which this
repo has already paid for once. Now asserts every page's MediaBox against Letter.
CARD COUNT. `if (!r.cards)` passed a packet carrying ONE card. Counts must stay relative
(a per-zone packet has its own group count), but relative does not mean unasserted:
sheets === 1 + ceil(cards/PER_PAGE) + 2 holds for any zone.
★ That was not enough, and the mutation test is what showed it. Deleting a whole list
sheet removed 15 cards AND one sheet, so the invariant still held — 80 cards / 7 sheets
is valid arithmetic — and the packet PASSED while missing 15 people groups. Internal
consistency is not completeness. So it now cross-checks an INDEPENDENT witness: the
{from}-{to} of {count} header the renderer prints on each list page, where count comes
from groups.length and does not move when a page is lost. It also requires the ranges to
TILE [1..count], which catches a missing MIDDLE page that the totals alone cannot see.
Those headers are TRANSLATED and the languages disagree on order — en/ar read "1-20 of
95" while hi reads "95 में से 1–20" and zh "95 之中的 1–20", total FIRST. A positional
regex would read those backwards. Parsed as digits with max() as the total instead, and
locale digit forms normalised to ASCII, so it is grammar-independent. Verified by
running the missing-middle-page mutation in Hindi as well as English.
MARK 5 WAS ASSERTED BY AN ENGLISH WORD. body.includes('Gadarenes') would go RED on a
correctly-translated packet, and passes today only because every locale falls back to
English for that key — the check and the bug were locked together. The renderer now
emits class="p4-passage"/"p4-credit" as test hooks (the block is styled inline and had
no selector at all), and the gate asserts length >= 1200, which holds in any language.
check:packets-all runs the gate over EVERY generated packet rather than one hand-named
file. The single-file gate SKIPs without an argument, so it was in package.json, passing
in all 11 locales, and in NEITHER gates:pre NOR gates:post — it ran only when someone
typed a filename. Now in gates:post (21/21, 11 packets through a real browser). SKIPs
when no packets are generated, so a clean checkout is unaffected.
Mutation-tested: A4 FAILs, a dropped sheet FAILs, a missing middle page FAILs in en and
hi, a truncated passage FAILs, a removed passage FAILs. i18n verified end to end by
adding a PARTIAL new language as a data block only — 3 of 23 keys — and confirming the
translated strings render, untranslated keys fall back to English, and nothing is blank.
|
||
|
|
e8e2be1498 |
Merge main into bundler-2/work
Some checks are pending
build-gate / build (push) Waiting to run
# Conflicts: # package.json # public/doxa-maps/all-people-groups/all-people-groups.js # public/doxa-maps/all-people-groups/all-people-groups/index.html # public/doxa-maps/index.html # public/doxa-maps/manifest.json |
||
|
|
9056b9329c |
All 12,379 pins were missing on MapLibre, and now a gate says so
Having found that MapLibre drops a whole layer for one unrecognised paint
property, I swept all 17 isMap leaves under ?provider=maplibre listening to
the map's own error event -- the only place this class surfaces. One more
real instance:
layers.apg-pins.paint.circle-sort-key: unknown property
circle-sort-key is a LAYOUT property, written into paint. Mapbox tolerated
the misplacement; MapLibre rejected the layer, so every pin on
all-people-groups was absent under the keyless engine while the page looked
perfectly healthy. Moved to `layout`; the sort order itself is unchanged.
Screenshots read on both engines: 12,379 pins, identical.
NEW GATE check:maplibre-drops loads every isMap leaf under maplibre and
fails on the layer/source-dropping error family. It skips cleanly when the
server is unreachable, because an UNMEASURED check is not a FAILED one --
the flaw already filed against check:filter-width.
★ THE NEGATIVE CONTROL WAS VACUOUS TWICE BEFORE IT WAS REAL, and that is
worth more than the fix. The first poison renamed the layout block, which
does not reproduce the bug -- the property simply vanishes, no error. The
second poison was faithful, circle-sort-key back in paint, and the gate
STILL printed PASS. Cause: I rebuilt with `BUNDLE=... vite build` and did
not run `bun run tree`, so the leaf carried stale SRI, the browser REFUSED
the script, no map mounted, and the gate saw zero errors. A check that ran
correctly and measured nothing, reading as green -- the first entry in
AGENT-BRIEF's troubleshooting table, walked into anyway.
After `bun run tree` (check:sri 0) the gate FAILED on the real bug, naming
the exact property, and PASSED again once restored. Both directions seen on
this codebase, which is the only thing that makes its green mean anything.
gates:pre 0, gates:post 17/18 (pre-existing verify/tables-complete),
check:doc-scripts 0, check:no-orphan-gates 0.
|
||
|
|
4cb04480cc |
Localize tab labels for it/ro/zh, and prove the catalog is actually read
Some checks are pending
build-gate / build (push) Waiting to run
120 keys across three locales: legend.json (12 each), search.json kinds (10
each) and tabs.json (18 each, previously absent entirely). Each locale follows
its own established vocabulary rather than a parallel one — Popolo/Popor/民族,
matching the terms already used in that catalog's detail and legend strings.
zh stays Simplified here, as the rest of this tree is, even though the prayer
packet's zh block is Traditional: a reader sees 民族 next to 祷告进度, never
the two files side by side.
Ceilings drop 94 -> 54 per locale. regions.json is the only i18n debt left in
the app: 54 keys of full sentences, three locales.
WHY THE NEW GATE. check:i18n-parity compares catalogs TO EACH OTHER, so it
certifies a file no t() call ever reaches — perfect symmetry, zero consumers,
green forever. Grepping for t('tabs. found ZERO call sites and tabs.json was
nearly recorded as dead. It is not: the only consumer builds the key
dynamically (SemanticTreeLegend.vue:40, 'tabs.' + (tab.i18nKey || tab.id)),
which no literal-prefix grep can see. The reverse error appeared in the same
pass — regions. and options. matched only poster-regions.mjs and a JS options
parameter. Static reachability is unavailable in both directions, so
check:tabs-localized renders the labels and reads them.
It asserts DIFFERENCE from English, not mere presence: te() falls back to the
hardcoded English label, so "shows some text" passes on the exact failure
being tested. Mutation-tested three ways — missing catalog FAILs, an all-English
catalog FAILs (first draft PASSed it, taking a per-pair exemption seven times),
one legitimate collision still passes with a note.
Both i18n gates now run in gates:post; neither was wired into the build before.
Also fixed, found by the build rather than sought:
- check-i18n-parity hardcoded an absolute path, so the studio copy silently
graded wt-packet's locales and reported PASS on a tree it never read. Now
repo-relative. That exposed the studio's own locale tree as a strict subset,
103 keys behind including buttons.json:share — the very orphan this gate's
header says it was written to catch, reappeared unseen in a second repo.
Synced forward: zero studio-only keys, zero text conflicts, lossless.
- index.json still declared field-access-visas with rows:1146, a leftover from
the commit that removed that table as private. The generator already filters
private tables; the committed artifact predated the filter. Removing the
declaration also removes the row-count leak. Copying the file in would have
reopened the leak.
- check:sfc-parse needs an installed node_modules and is the first pre-gate to
fail without one, so a dependency-less worktree reads as a parse error.
Documented, with the reason switching it to bun only appears to fix it.
|
||
|
|
ce750f5b55 |
test(i18n): a key in English must exist in every locale — ratcheted
Some checks are pending
build-gate / build (push) Waiting to run
The failure here is silent BY DESIGN. vue-i18n falls back to English for a missing key, which is the right runtime behaviour and exactly why catalogs drift unnoticed: the app looks fine, nothing errors, and a reader who chose Spanish just gets English words with no signal anything is missing. Measured when this was written: buttons.json had 21 keys in English and 19 in every other locale. The orphans were `share` and `shareSteps` — the Share button's title and its explainer, rendered on EVERY map. Ten locales had been shipping an English Share button since those keys landed. ★ IT FOUND MUCH MORE THAN THE BUG THAT PROMPTED IT: 429 missing keys across ten locales, with popup.json existing only in English and it/ro/zh also lacking regions.json and tabs.json entirely — 101 of 176 keys each. So it RATCHETS rather than blocks. Each locale has a ceiling; the gate fails when one gets WORSE and prints a nudge when one gets better so the ceiling can be lowered. Failing outright on 429 keys of debt this gate did not create would block every build, and a gate everyone disables protects nothing. A locale with no ceiling entry must be complete, so a NEW locale cannot be added half-done. Keys are compared FLATTENED, so a nested object or an array is checked key-by-key rather than counted as one present key. Confirmed to fail before acceptance: deleting two French keys reports "fr regressed: 8 missing key(s), was 6" and names them. |
||
|
|
39ec4b42bd |
test(i18n): every packet language must be SELECTABLE, not merely present
Some checks are pending
build-gate / build (push) Waiting to run
The bug this gates is unreachability, not incorrectness. The packet shipped 11
complete translations while detectLocale() read only <html lang>, which every
generated leaf page hardcodes to "en" — so ten of them could not be reached by
any reader on the static tree. Nothing was wrong; nothing was visible.
Two assertions, because they fail separately:
RESOLUTION ?lang= / ?locale= selects a supported locale, an unsupported
value falls back to English, a region tag (pt-BR) resolves to its
base, and with no param the host page still wins.
REACHABILITY the packet's language list and SUPPORTED_LOCALES cannot drift
apart, and the leaf pages still declare a lang — so if someone
later drops that attribute, the host-page path does not silently
become unavailable while the tests stay green.
★ It evaluates the SHIPPED detectLocale rather than reimplementing it. A gate
that reimplements the thing it tests only ever proves the copy agrees with
itself — which is how a fix can be reverted in the source while the gate keeps
passing.
Confirmed to fail before acceptance: removing the URL branch reports all four
param cases resolving to "en".
|
||
|
|
e263991dc5 |
test(deck): click the REAL button and read what it prints
Some checks are pending
build-gate / build (push) Waiting to run
Every other packet/card gate drives the RENDERERS. This one drives the UI: it finds the Card Deck button in the shadow root, clicks it, and captures the document that reaches the print frame. That is the only path proving the wiring — records provider, stock picker, language, shared composable, print frame — is connected end to end. A renderer can be perfect while the button calling it passes the wrong stock, or nothing at all. ⚠ It intercepts the printed HTML rather than reading the iframe back. openPrintFrame calls window.print(), which BLOCKS in headless Chromium, and the frame self-removes on afterprint — reading it back measures a frame that has already gone and reports 0 cards for a correct deck. Measured through the real UI: 116 cards at 2.5in 3.5in with 116 QR codes, and 3.5in 2.5in when the stock picker is switched to landscape. Confirmed to fail on a wrong expected page size. Also updates the share policy: country-posters moves from 'own-button' to 'header'. Its two-tab MFE now owns sharing, exactly as my-upg-100-list's does — the gate caught the bundle briefly carrying two affordances after the MFE landed, which is the regression it was written for. |
||
|
|
449adfe515 |
test(poster): the shared print actions must produce real documents
Some checks are pending
build-gate / build (push) Waiting to run
Gates usePosterPrints.js — the print actions now shared by every poster wall — by driving them in a browser with a fake records provider, so the assertion is about the DOCUMENTS rather than any bundle's API being reachable. ★ BOTH DIRECTIONS, because "the button is there" and "the button works" are different claims and this flight has repeatedly found the first true while the second was false. With a provider: a real table, correct row count, the prayer checkbox column present. WITHOUT one: phase "error" naming the missing provider, not a silent no-op and not a throw. ⚠ IT CAPTURES THE PRINTED HTML INSTEAD OF READING THE IFRAME BACK, and finding that out cost a wrong first result. openPrintFrame calls window.print() under autoprint, which BLOCKS in headless Chromium, and the frame self-removes on afterprint — so reading it back measured a frame that had already gone and reported 0 rows for a perfectly correct document. Intercepting what the composable hands to the frame measures what actually gets printed. Confirmed to fail before being accepted: deleting the checkbox column from the row template reports "the list print has no prayer checkbox column". |
||
|
|
e1553df458 |
test(provider): ?provider=maplibre must swap the ENGINE, measured by request host
Some checks are pending
build-gate / build (push) Waiting to run
Gates BUG-UPG100-IGNORES-THE-PROVIDER-SWITCH. my-upg-100-list builds its map RAW with `new gl.Map` instead of going through useMapInstance, so the page-level switch — which resolves inside that composable — could not reach it. The card measured 19 requests to api.mapbox.com at ?provider=maplibre while five sibling leaves swapped cleanly. ★ IT COUNTS NETWORK REQUESTS BY HOST, not globals. `window.mapboxgl` can be present because the leaf page preloads it while the map is actually DRAWN by maplibre, and the reverse is equally possible. What the switch exists for is not loading a proprietary engine and not calling its servers, and only the request log shows that. The allowance is DERIVED from the leaf's own HTML — count the distinct api.mapbox.com urls the generated page declares, plus one for a stylesheet that gets fetched twice — rather than hardcoded. Asserting merely "fewer than the default" would pass a half-fix at 18 of 19; hardcoding 5 would pass a leaf that preloads 4 and leaks 1 runtime call. ★ THE GATE'S OWN FIRST VERSION MEASURED NOTHING AND PASSED. `const URL = process.argv[2]` shadowed the global URL constructor, so every `new URL(request.url())` threw, the host tally stayed empty, and it cheerfully reported "0 mapbox requests" on a page making 19. It now names the variable LEAF, counts what it could not parse, and FAILS if the default run records no requests at all — because a silently empty tally is indistinguishable from success. Passing on three leaves today: upg-100-list 19 -> 5, doxa-simple-map 21 -> 4, countries-map 24 -> 4, with every residual being the page's own <link>/<script> preloads. Confirmed to fail when the allowance is tightened by one. |
||
|
|
c9ed3b4e9a |
test(mfe): a two-tab element must mount both panes AND occupy space
Some checks are pending
build-gate / build (push) Waiting to run
Gates the composite MFEs at the rendered layer. The failure it exists for is
BUG-UPG100-NESTED-ELEMENTS-ZERO-HEIGHT: every layer inside these elements is
position:absolute/height:100%, so the chain resolves to ZERO unless :host is
sized. The header renders, the tab strip paints, the panes mount — the element
looks alive and occupies no space. A DOM-presence check passes it.
★ THE HOST HEIGHT IS THE LOAD-BEARING ASSERTION, and finding that out cost a
wrong first attempt. I wrote the gate to check PANE heights, which is the
obvious reading of "zero-height collapse" — then measured it: squashing the host
to 0px leaves its panes still reporting 843px, because they are absolutely
positioned and escape the collapsed box. A pane-height check alone does NOT
catch the bug. Only measuring the host does. The pane check stays as a secondary
signal with a note saying why it cannot be trusted alone.
Also asserts exactly one share affordance (two on one surface shipped once as
BUG-UPG100-DOUBLE-SHARE) and that the strip has the expected number of tabs.
Tabs are selected by role=tab, not by class. '.viewsel .seg' matched the tablist
WRAPPER as well as its buttons, reporting 3 tabs for a 2-tab strip with
concatenated text ("Country PostersMap") — the gate finding a bug in itself.
Passing on all three composites today: upg-100-mfe, regions-mfe and the new
country-posters-mfe. Confirmed to FAIL before being accepted.
|
||
|
|
8e8e659556 |
test(share): one decided rule for which maps carry a share button
Some checks are pending
build-gate / build (push) Waiting to run
Driver: "Some maps need a map-level share button and some do not... Decide +
record the per-map rule, then apply it."
The BEHAVIOUR was already right on both maps the card names — Prayer Progress
(an instance of doxa-simple-map) has its button, and UPG-100 deliberately omits
one because upg-100-mfe.vue's header owns sharing, with a comment saying so.
What did not exist was the RULE. Nothing stopped the next agent adding a second
button to a surface that already had one, and nothing noticed a published map
with no way to be shared at all.
Surveying all 11 map bundles found FOUR patterns in use, so the policy is
written as the four it actually needs rather than a binary:
own-button the map IS the product surface 6 bundles
header a composite header owns sharing my-upg-100-list
composite a parent overrides shareUrl so the
PARENT is shared, not the child research-map-plus-grid
none unpublished, nothing to share yet key-cities
★ IT FOUND TWO REAL GAPS the card did not anticipate: all-people-groups and
prayer-walk-planner are PUBLISHED and have no sharing path by ANY mechanism —
no button, no shareUrl, no composite parent. They are recorded as
'debt-no-share' so the gate passes on today's reality while NAMING the gap, and
so the count cannot drift without someone deciding. Silently passing them would
have buried the finding; failing on them would block a green build over
pre-existing debt this card never scoped.
A map bundle with no POLICY entry FAILS, so adding a map forces the sharing
decision instead of deferring it. Both negative cases confirmed to fire before
this was accepted: a second <ShareButton> reports "two affordances on one
surface" (that shipped once already), and removing an entry reports the bundle
as undecided.
|
||
|
|
d050f7e216 |
test(zones): census every SELECTABLE zone against the posters built
Some checks are pending
build-gate / build (push) Waiting to run
The failure this catches is a short count with no error. generate.mjs used to
abort on the first bad zone — 16 of 29 written, the rest silently unbuilt — and
separately, tooling that enumerated clusters.json rather than the LEGEND warned
two zones were "MISSING" when nothing was. Both look like success from a
distance; only a census shows them.
★ ASK THE LEGEND, NOT THE DATA FILE. clusters.json carries 30 list definitions;
the legend exposes 28 leaves. deaf-old-world and deaf-new-world-asia-pacific are
collapsed into the unified `deaf` row (affinity bloc A017) — VERIFIED against
live records rather than taken from a comment: 63 + 60 = 123 = A017 exactly,
with zero groups on either side of the difference. They remain in clusters.json
deliberately, because three isolation-proof scripts assert
deaf_lists.length === 2, but no user can select them.
So they are reported as CONTEXT ("superseded, not a gap"), never as a failure.
Counting the file instead of the legend manufactures two phantom failures and
then hides real ones behind them — which is how a genuinely missing poster would
get lost in noise everyone had learned to ignore.
Passing: 28 selectable zones, 28 posters. Confirmed to fail before being
accepted: removing one poster reports that zone by name.
|
||
|
|
347823a92b |
test(print): each print path must produce its OWN page geometry
Some checks are pending
build-gate / build (push) Waiting to run
Driver: "make sure poster generation works and the aspect ratio of the print
dialog can be custom based on your print point, so print dialog works for
poster, for vertical packet, and for horizontal cards... all working in the
jamstack maps."
One gate over all four paths, because they share one failure: silently falling
back to letter. Chromium does that for ANY @page size it does not recognise
('A6 portrait' -> 612x792pt, no error, no warning), so every assertion here is
on the MEASURED MediaBox from page.pdf(), never on the stylesheet. A gate that
grepped the CSS would pass a deck that prints as paper.
Measured, all from the shared renderers the UPG-100 page itself imports:
packet (vertical) 612 x 792 pt letter portrait
cards (poker) 180 x 252 pt 2.5 x 3.5in
cards (landscape) 252 x 180 pt 3.5 x 2.5in
poster 1500 x 2000 px rasterised, painted
Orientation is asserted separately from size, so a transposed stock cannot pass
by accident, and the poster is checked for ink rather than dimensions because it
rasterises rather than paginating.
|
||
|
|
d638544870 |
test(posters): a poster with no pre-rendered image must still download
Some checks are pending
build-gate / build (push) Waiting to run
Gates the client-side poster fallback, and it measures PIXELS rather than bytes — because the failure mode here is silent and total. ⚠ An SVG loaded through <img> is an ISOLATED DOCUMENT. It cannot see the host page's CSS, so every var() falls back to its initial value, and `fill`'s initial value is BLACK. A poster that loses its stylesheet downloads as a black rectangle with no error, no exception, and a perfectly normal file size. The prayer packet shipped exactly that bug once already (660 of 661 map elements painting black, nothing in the console). So the gate asserts mean luminance and colour variety, not "did it produce bytes", and it checks the stylesheet actually travelled into the inlined SVG. Confirmed to FAIL on the real regression before being accepted: with the <style> block stripped, luminance drops 229 -> 13 and distinct colours 54 -> 2, and all three assertions fire. Passing: laos rasterises to 3000x4000, 725 KB, stylesheet inline, luminance 229, 54 distinct colours. |
||
|
|
a9faab1662 |
test(cards): a deck must print as CARDS — assert the measured page size
Some checks are pending
build-gate / build (push) Waiting to run
Gates the prayer-card deck at the rendered layer, and asserts two things a
plausible-looking check would miss.
1. THE MEASURED MediaBox, not the CSS. Chromium SILENTLY falls back to letter
for a named @page size it does not know — `size: A6 portrait` yields
612x792pt with no error, while `size: 2.5in 3.5in` yields exactly 180x252pt.
A gate that grepped the stylesheet would pass a deck that prints as paper,
which is the entire bug being fixed. It also names the failure: if the page
comes back 612pt wide it says "this is LETTER: the @page size silently fell
back".
2. THE VALUES ARE HUMAN-READABLE, not merely present. The first deck rendered
"LAO", "thm", "EAN" because normalizeRecords() flattens { value, label } to
the VALUE and parks the label under <field>_label. Counting rows passed it —
the rows existed, they were just codes on a card meant for praying. The gate
now flags any field showing a bare 2-4 character token.
Also asserts one card per page, portraits actually loading, a real QR symbol per
card (not an empty path), and no card overflowing its own box.
Expected page sizes are DERIVED from the stock's inches rather than hand-copied:
an earlier constant said A6 was 396.5x559.7pt and FAILED a correct render — the
gate's own arithmetic was wrong, not the renderer.
Verified across all three stocks: poker 180x252pt, postcard 288x432pt, A6
297.12x420pt, 95 cards / 95 pages each.
|
||
|
|
097ab02458 |
test(packet): gate the packet the READER gets, not the one Node writes
Some checks are pending
build-gate / build (push) Waiting to run
check:packet-render verifies a packet FILE from the build-time generator. This
verifies the BROWSER path — the legend's Export > Prayer packet action, which
imports library/packet/packet-render.js, fetches live records and prints
in-place.
Both callers now share that one renderer, and this gate is what proves the
shared module actually runs in a tab. A module can be perfectly correct under
Node and still fail in a browser: a bare process.env reference, a Node-only
import, a JSON import assertion. Node passing says nothing about that.
It drives the real thing: the renderer over HTTP, the app's own normalizer
(the API returns { posts, total } — hand-rolling that extraction is how a probe
ends up testing something the product never does), live records, then
page.pdf() and a PDF page count.
Measured on the browser path: 8 sheets -> 8 pages at 612x792pt, 95 people
cards, 12 wheel segments, Mark 5:1-20 in full, exactly 2 metacamp.org
attributions, 0 page errors.
I18N is asserted here because it is a product requirement, not a nicety: pass a
language and the gate reports which headings came back translated. Verified with
a partial Spanish block — "12 Puntos de Oración" and "Rueda de Oración" rendered
while Mark 5 and both attributions correctly fell back to English. Fallback is
PER KEY, so a half-finished translation renders its finished keys and never
blanks. The stub block was removed after verifying; shipping it would imply a
Spanish translation that does not exist.
Parameterised rather than pinned: PACKET_ORIGIN / PACKET_API / PACKET_ISO3, and
the language as argv. Needs a static server over the repo root.
|
||
|
|
4d9ac89f6e |
test(packet): gate the prayer packet at the RENDERED layer
Some checks are pending
build-gate / build (push) Waiting to run
Every bug this gate exists for produced a file of the right size, with no
error, no broken image, and a generator reporting success. They were only
visible once a browser laid the document out, so this drives a real browser
and reads computed style rather than inspecting the HTML source.
★ The law it encodes: never print a number without asserting on it. The
throwaway probe this replaces PRINTED "PDF pages: 16" for several runs while
reporting "packet verified", because the failure counter never looked at that
value. A number that is displayed but not asserted on is decoration.
Two regressions it catches, both confirmed to FAIL before this was accepted:
transform instead of zoom -> 16 PDF pages for 8 sheets
map stylesheet removed -> unresolved --paper/--sea/--frameline;
660/661 map elements fill BLACK
The second is the subtler one. The map SVG carries almost no presentation
attributes; it is styled from the poster document's stylesheet. Lift the
<svg> without those rules and SVG fill falls back to its initial value, which
is black — the whole map paints as a solid rectangle with nothing to see in
the DOM, no console error, and no broken image. It is asserted as a
WHOLE-MAP property because the first attempt tested one hand-picked <rect>
(the one just repaired) and went green while 647 others were still black.
Page count is load-bearing rather than cosmetic: the packet is printed
front-and-back, so "one sheet is exactly one page" is a correctness property,
not a tidiness one.
Counts are relative (sheets vs pages, cards vs photos) so the gate is not
pinned to one zone's group count, and the packet path is an argument or
PACKET_HTML rather than a hardcoded path. With no packet given it SKIPS
rather than failing, since the packet is generated on demand.
|
||
|
|
a334e8ebff |
Merge main into bundler-1/work
Main moved 20+ commits while the regions work was on its own branch. Every
conflict was a GENERATED file — manifest.json, the root page, and the
regions-dashboard bundle's .js and leaf pages. Zero source conflicts: all six
of my source files (the two regions .vue, the new map child, fix-sri.mjs,
sri-consistency-check.mjs, package.json) auto-merged.
Generated files are not hand-merged. They were resolved to main's side and then
REGENERATED from the merged source — BUNDLE=regions-dashboard vite build, then
bun run tree, then bun run fix:sri. `bun run tree` rewrote every bundle's pages
and produced byte-identical output to main's committed ones for all twenty
other bundles, so the only files this commit changes beyond the merge are the
six that had to be rebuilt. That agreement is also evidence main's committed
tree matches main's source.
The pinHint:false regression I saw before is gone: main's
|
||
|
|
20a3d3d245 |
Run the verification chain in phases instead of one long queue
The 29 gates in build:unlocked cost 84.5s chained with &&, but 21 of them
finish in under 700ms — they were queueing behind check:mjs-parse (26.9s)
and check:parse (18.7s) on an otherwise idle 8-core box. The 22 vite builds
cost 129.0s and were never ordered at all: vite.config.js gives each bundle
its own outDir with emptyOutDir:false and emits nothing outside it, so they
only ever needed to not overwrite each other.
internal/run-parallel.mjs runs a set of independent steps concurrently and
grades the set by exit code. build:unlocked now reads:
gates:pre -> library-index -> clean -> token -> build:bundles -> tree
-> gates:post -> gallery
Measured end-to-end, both chains green, two rounds each, clean+token
excluded as common to both: 165.7s -> 65.3s mean (170.7/160.8 vs 60.2/70.5).
No gate was removed, weakened, sampled, or made conditional on what changed.
The step set is identical — 31 in, 31 out, asserted at edit time — and each
step runs as the same command graded by the same exit code. Only start times
moved. The && BETWEEN phases is load-bearing and stays: a source gate still
blocks the build, and the build still blocks every gate reading its output.
Within a phase the runner reports ALL failures rather than stopping at the
first, so three broken gates cost one fix-and-rerun cycle instead of three.
It imposes no timeout of its own, because a command killed by a short
timeout is not a failure.
Negative-tested three ways, each required to exit 1: a failing gate inside a
phase, a red phase halting the chain, and a failing vite build inside
build:bundles. The bundle case also confirmed the runner enumerates
plugin-designs live (it picked up 23 dirs, not a cached 22).
|
||
|
|
6734be1ad6 |
Repair SRI hashes without a full rebuild
check:sri failed whenever a single-bundle `BUNDLE=x bun x vite build` left a leaf's baked integrity behind its bundle — and the gate then told the agent the fix was `bun run build`, whose first step deletes public/doxa-maps/ and tears the tree out from under every concurrent reader. Adds `bun run fix:sri`: rewrites the disagreeing hashes in the built tree and nothing else — leaf <script integrity>, manifest bundles[].integrity, and the paste-verbatim embed snippets. 0.4s, writes only the offending pages, and the bytes it writes are identical to what the full build wrote. check:sri's failure text now prescribes that instead of a rebuild, and its header no longer claims generate-tree runs only inside the full build (it does not; `bun run tree` re-bakes the hashes too, at the cost of rewriting 1234 files under public/ and re-reading .vue source every run). Negative-tested: `node internal/fix-sri.mjs --self-test` builds a poisoned synthetic tree, requires check:sri to go RED on it first, then green, then a second repair to write zero files. |
||
|
|
10fdda338b |
Rebuild the tree so every leaf's baked SRI matches its bundle
Some checks are pending
build-gate / build (push) Waiting to run
A single-bundle build regenerates the JS but not the leaf pages, so four big-picture leaves carried a stale sha384 and the browser refused to execute the script at all. That is why a nav probe measured 0 sections on a shell whose source was correct, and it is the likeliest cause of a teammate's 'pggMounted: false on big-picture everywhere' reading. check:sri named both the cause and the fix; ran the full build it prescribes on a settled tree. Now PASS: 44 script references across 45 leaf pages, every leaf's integrity matching the bundle it loads. |
||
|
|
f48cad8dbb |
Widen the red ramp, add a greyscale ramp, and gate cap-4 interoperability
Some checks are pending
build-gate / build (push) Waiting to run
THE RAMP. The Driver reported that 60-149 reads as the same red as the 150+
heartbeat bin. The old ramp was mathematically even and he was still right --
that tension was the finding, not a contradiction. Measured in plain sRGB
distance the top step (60.0) looked like the LARGEST in the ramp; that
measurement was lying, because RGB distance is not perceptual. Re-measured as
deltaE the old steps were 15.5 / 15.8 / 16.7 / 17.7 -- nearly flat, while chroma
collapses at the dark end so hue and saturation stop carrying information there.
The new ramp deliberately ACCELERATES toward the dark end: 14.1 / 17.9 / 21.4 /
24.9. Top-bin separation 17.67 -> 24.94, a 41% improvement on exactly the pair
he named, and total lightness range widens from 37.2 to 49.0 L*.
A named greyscale ramp ships alongside it, grey to black, as he asked ("just in
case i need to swap from red to black scale"). Its steps measure 22.8 / 22.4 /
22.4 / 22.8 -- near-perfectly even, every step well above the ~10 threshold for
clear distinguishability. It is an OPTION, selected by name; the red ramp
remains the default, so no prior decision is silently reversed.
Bin EDGES are untouched -- check:heartbeat passes, which replays the bin
assignment and fails if a default moves.
THE INTEROP GATE. verify:interop drives the actual compiled cap-4 composites in
a real browser against live data, closing the gap verify:gate1 leaves open. On
its first run it PASSED both desktop directions: a facet-rail click narrowed the
grid to "Showing 1,309", and selecting three rows moved the camera from
28.000,18.000@1.45 to 73.666,13.162@2.48 -- read back off the live map, not
inferred. It asserts the grid's live count line rather than card count, because
card count is a proven false-negative (always 24).
It ships with verify:interop:self-test, a two-mode negative test -- grid-prop
freeze and camera freeze -- each required to fire its own combination-named
failure. A harness that has only ever reported PASS is unproven.
Gates: sfc-parse, template-literal, one-palette, synced-css, mjs-parse,
heartbeat, doc-scripts all pass.
|
||
|
|
718ad723dc |
Check that the numbers the maps show match the numbers the source has
Every gate in this repo verifies INTERNAL consistency. check:facet-changes-map proves the dots change and that the categories sum to 2,107 — but it asserts that inside the page, against data the page already loaded. If the upstream corpus shifted, the page would stay perfectly self-consistent while its legend quietly stopped describing reality. Nothing fetched the live endpoint: not the six data guards, not check:label-joins, not any browser gate. That is the worst failure this product can ship, because it is invisible to every structural check. The element mounts, the canvas paints, the counts are formatted correctly, the partition sums. Only comparison against the SOURCE catches a legend that is beautifully wrong — and these maps exist to say where the gospel has not reached, so a wrong number is not a cosmetic defect. Measured by hand first, then encoded. The cap-2 engagement legend shows Unengaged 2,080 / Engaged 27; the live API returns 2,107 rows splitting exactly 2,080 / 27, with no third bucket and no remainder. The cap-4 dashboard KPI strip prints "1% ADOPTED"; the live API gives 21 of 2,107, which is 1.0%. Three independent surfaces agreeing to the unit. The gate also asserts the partition is COMPLETE and that no category exists which the two-row legend does not show — a third engagement value would hide rows from the reader while every count still looked right. The expected values are a baseline, not a law, and the failure message says so: the corpus is living data and these numbers will move. A mismatch is a prompt to update the legend, the KPI and the baseline TOGETHER, which is the coupling that was missing. Updating the baseline alone would silence the gate and leave the maps wrong. Offline SKIPs at exit 0 rather than failing, because reporting a mismatch that may not exist is the fail-open mistake in reverse. Negative-tested both ways: drifting the baseline fails naming the exact category and both numbers; an unreachable host skips and says why. The browser user-agent is deliberate — pray.doxa.life answers 403 to a default script UA, a trap already recorded here, and one that presents as "the data is gone". |
||
|
|
4c8f6b4a6d |
Share the mobile composite too, and make the url absolute so the embed works
research-map-plus-grid had the same defect fixed yesterday on its desktop
sibling: sharing the composite emitted a link to doxa-research-map, the child it
nests. The gate said it exactly — "share URL reopened <doxa-research-map>, not
<research-map-plus-grid>" — and a recipient got the map alone, losing the grid.
Checked all four cap-4 composites rather than assuming the fix generalised.
my-upg-100-list owns its Share button directly and is fine. big-picture composes
a header plus the grid, owns no map and ships no Share button at all; the gate
reports "window.mapboxgl UNDEFINED" against it, which is its map precondition,
not a share failure — that bundle is out of this gate's scope and I put it in
the probe by mistake. So one real instance, now fixed.
Tier 1 again, for the same reason: this child also hardcodes a prop
(research-map.vue:2149 `:embed-path="'doxa-research-map/research-map/index.html'"`)
and the ladder is `embedPath || cfg?.shareEmbedPath`, so a prop outranks host
config and tier 2 is unreachable. Delivered through the profile-config attribute
rather than Vue provide, because this composite mounts its children as real
custom elements via src — there is no provide chain to reach them.
The url had to be ABSOLUTE, and the first version was not. resolveShareUrl
returns a tier-1 shareUrl verbatim (embedBase.js:95, `return whole.trim()`, no
resolution), and ShareButton feeds the same string into its embed snippet:
<iframe src="{shareUrl}">. A relative '../../…' would have resolved correctly in
the share LINK, because the browser resolves it against this page, and then
404'd in the EMBED, because it resolves against the partner's page instead. That
would have passed the gate — the gate navigates the link — while breaking the
deployment method the link exists for. Tier 1 is documented as "a WHOLE url" for
precisely this reason. Now computed with resolveEmbedBase() so it stays correct
on doxa.life, on nuxtbase.app and in a dropped tree.
Verified the desktop sibling did not have the same latent flaw: it produces a
fully absolute url, confirmed in the live gate output.
smoke:share-all now carries eleven targets. Full suite 11/11 pass · 0 fail.
Eleven gates pass, including sri, composite, composite-srcs and synced-css.
|
||
|
|
fe8e785c96 |
Share the composite, not the child it nests
Sharing from dashboard-plus-grid produced a link to research-dashboard/research-dashboard/index.html — its own nested child. A recipient opened the inner dashboard alone and silently lost the grid half of the composite. The gate said it precisely: "share URL reopened <research-dashboard-map>, not <dashboard-plus-grid>". The nested dashboard's DashboardTopBar owns the Share button, and inside the composite that button had no idea it was a passenger, so it shared its own address. Two attempts, and the first one is worth keeping in the record because it built and shipped and changed nothing. I set profileConfig.shareEmbedPath — tier 2 of ShareButton's documented ladder, and the natural-looking channel. The ladder is `embedPath || cfg?.shareEmbedPath`, and DashboardTopBar passes an explicit prop (`<ShareButton :embed-path="SHARE_EMBED_PATH" />`), so a child that has already made up its mind always wins. The value was baked into the bundle, resolveShareUrl reads that key, and the rebuilt bundle failed the gate identically — a fix that is present in the artifact and unreachable at runtime. Tier 1, `shareUrl`, is the only rung above a prop, and its docs say exactly what it is for: "a WHOLE url the HOST supplies wins". Tier 1 takes a whole url, so it is computed rather than hardcoded, via the same resolveEmbedBase() helper ShareButton itself uses: profileConfig.embedBaseUrl → window.MAP_EMBED_BASE_URL → the root the built bundle derives from its own script url. The link therefore stays correct on doxa.life, on nuxtbase.app and in a dropped tree with no configuration. An absolute url would have pinned every share to whichever host happened to build it. Also adds the two uncovered bundles to smoke:share-all. Nine bundles ship a Share affordance and the hardcoded --url list covered seven; research-dashboard and dashboard-plus-grid were absent, which is why this survived. The list is now ten targets over nine bundles. Note the structural difference from smoke:embeds, which enumerates from manifest.json and therefore cannot omit a bundle that ships later — a hardcoded list silently omits whatever arrives after it was written, and that is what happened here. This is the two-host defect class exactly: research-dashboard standalone PASSES this gate, and the identical component inside the composite FAILED. No single-host gate can tell those apart. Verified: the composite now reopens <dashboard-plus-grid> with a canvas, and the full suite is 10/10 pass · 0 fail, so nothing regressed. Eleven gates pass, including sri, composite, composite-srcs, synced-css and one-palette. The share gate aborts rather than reporting a false failure when handed Firefox, which has no WebGL here — it told me to re-run with chromium, and it was right. |
||
|
|
8550e58fbe |
Stop the browser-gate aggregator's description from counting
package.json described verify:browser-all as running "ALL 7 browser gates". The GATES array in internal/verify-browser-all.mjs holds nine. Two were appended below the count — check:fixed-anchoring and check:shell-scroll, both added the same week — and the prose was never touched, so a reader who ran the command and tallied seven would have concluded two gates had not run. This is the same shape as the runbook's "all 6 DX guards PASS" corrected earlier today, and the fix is the same: stop stating a number that lives in another file. A prose count of a list that grows elsewhere rots on the next append, and it rots silently, because nothing compares the two. The description now says it runs every gate in the array and explains why it refuses to give a figure. The script already prints its own GATES.length at runtime, which is the number that cannot be wrong. Also repairs a garbled sentence in the same string — "Only smoke:share the `verify:browser-all:plugin-gallery` script mode is out of scope" appears to be two edits collided. It now says plainly that the plugin-gallery mode is separate because it needs the vite dev server. One line changed; check:quoted-scripts and check:doc-scripts both pass, which matters because those gates compare this file against the docs that quote it. |
||
|
|
4b39abf126 |
Regenerate the library index during the build, so it cannot fall behind
check:library-index existed, was correct, and had been failing for about 19.6 hours. Nothing noticed because it runs only when a person types it: build:unlocked chains 24 gates and this was not one of them. check:no-orphan-gates passes on it, which is worth understanding rather than treating as a gap in that gate. It asks whether every gate is REACHABLE — "named in package.json or imported by something that is" — and check:library-index is named there. Reachable and wired are different properties. 64 gate-shaped files exist; 24 run on every build; the rest only run on request. The fix is to regenerate rather than to check harder. check:composite is also mtime-based and is in the chain, but it guards build output whose freshness depends on build ORDER — a real invariant only a human can resolve. reference-library-index.md is DERIVED from library/, so a staleness failure has exactly one remedy: run the generator. Wiring the check alone would fail builds and hand the author a chore; wiring the generator makes the staleness impossible, which is the only way the file's own header — "DERIVED — cannot drift from the code" — can be true. Both are now in the pre-build group, generator then check, beside the other source-only gates and before `clean`. Safe to run unconditionally: measured deterministic (byte-identical on re-run) and 237ms. The check is kept after it so a generator that silently writes nothing still fails the build rather than passing quietly. Updating package.json made check:quoted-scripts fail, which is the gate doing its job, and it surfaced that the quoted copy in README-SECTION-3 was ALREADY stale before this change: it omitted check:tab-radius, check:synced-css, check:wp-embed and check:doc-scripts. An omitted step in a quoted list is invisible — there is no wrong character to notice, only a check the reader believes is not in the chain. The doc now matches what runs. Verified by a full build: the generator ran, the check passed, vite built every bundle, exit 0. The index is not dirty afterwards, which is the determinism claim holding inside the real chain rather than in an isolated test. Five lines in the build log match /error|FAIL/ and all five are gate DESCRIPTIONS containing those words, not results. |
||
|
|
5b3618d0d6 |
Check that the CLI doc still quotes package.json correctly
Applying last tick's lesson -- grep for the class, not the instance -- found a third surface teaching the old serve command. README-SECTION-3-cli-commands.md transcribes the package.json scripts so a reader need not open the file, and it still showed python3 -m http.server two commits after serve became serve-strict.mjs. An inheritor following the CLI reference would have started the permissive server, the one whose forgiveness hid a URL bug in two gates for a whole flight. Measured before fixing: ten quoted scripts, nine matching, exactly one drifted. I had expected build:unlocked to be stale too and it is not -- the doc truncates it rather than misquoting it, which is the right call for a seven-hundred-character line. Hand-fixing the one line would leave the next script change to rot the same way, so the doc is now checked. The comparison is loose in the three ways a human-facing doc needs: package.json's trailing hash comments are ignored, the doc's own slash-slash annotations are ignored, and truncation passes on a prefix match either way. A quoted script that does not exist in package.json is a hard failure, since that is a command someone would type and get "script not found". Completeness is deliberately not asserted; the doc is a curated subset and demanding otherwise would make it churn on every internal gate. Self-test poisons the doc in memory two ways, a changed body and a nonexistent script name, and requires both to be caught. Both are. Full build passes with the gate in the chain. |
||
|
|
9d6b0ecd0f |
Run the WordPress contract test as part of the build
check:wp-embed existed as a script and nothing invoked it -- the same orphan shape as check:synced-css, which had been red and unrun for weeks. This one is green and unrun, which is quieter but no better: a gate the build does not consult is documentation. It is worth running. It renders the real shortcode against stubbed WordPress functions and asserts the output against the real built manifest, so it needs neither MySQL nor Docker. Its load-bearing assertion is that the wrapper emits the same SRI integrity hash the catalog ships, which is what gives the WordPress path the same tamper protection as the direct-script embed. It also carries a negative case: a malformed integrity value must be rejected. Full build passes with it in the chain. |
||
|
|
0f0bebd9c2 |
Serve locally the way production serves, so a bad URL fails here instead of live
bun run serve was python3 -m http.server, which resolves a directory request to its index.html. The live static layer returns 404 for the same request. So a gate could pass locally on a URL that does not work in production, and nothing local could ever catch it. That is not a harness quirk, it is a blind spot with a green light on it: every defect inside the permissive gap passes locally by construction and can only be found in production, which is the worst place to find it. It cost a false "the publish failed, zero filters live" reading on a publish that was byte-for-byte correct. serve-strict.mjs returns 404 for a directory, exactly like production, with no index fallback and no directory listing. It also refuses to read outside its root. Loopback only, inherited from the script it replaces, whose own comment records that the bind was once 0.0.0.0 and answered on the box's public IP while a gate was running. The permissive server is kept as serve:permissive rather than deleted, so the difference can be demonstrated. It earned itself immediately: verify-shell-scroll held two bare-directory URLs and failed the first time it ran against the strict server. Both now ask for index.html, which is the form the manifest already ships for all 28 profiles, and the gate passes again -- both wheel gestures moving the grid 1500px onto screen. Fixing the server rather than the URLs is the point. The repo was already production-correct: 28 of 28 manifest URLs and 82 gate references use index.html. What was missing was anything that could fail when one did not. A rule you have to remember fails under time pressure; a server that returns 404 does not. |
||
|
|
1652559e06 |
Teach the synced-CSS gate what a selector is, and turn it on
The gate reported 14 conflicts across two files. Thirteen were false and it was blind to real ones, and both came from a single cause: it keyed a rule on the last class before the brace, so any qualified rule was filed under its own base rule. A dark-mode override, a .dsm-dark variant and a :host(:fullscreen) rule all merged into the unconditional rules they were meant to override, and the pair was then reported as the file disagreeing with itself. In every case both copies were identical and the code correct. Merging is last-write-wins, so the same bug also HID conflicts. Poisoning .dsm-root inset from 0 to 7px in the shadow copy produced no finding at all, because the fullscreen rule's own inset:0 was merged in afterwards and restored it. A property could be shared, genuinely divergent, and invisible. The key is now the full selector, and at-rules are part of it. The header already claimed media-query variants were treated as distinct rules; nothing implemented it, and PeopleGroupDetail's 18px name with a 16px @media override read as the file contradicting itself when the shadow copy simply has no mobile rule -- which this gate's own contract explicitly allows. At-rule bodies are brace-matched rather than regex-matched, since a lazy pattern stops at the first inner brace. The shadow literal is also sliced AFTER its opening backtick. Slicing from the anchor left useShadowStyles(` at the head of the CSS, which a full-selector pattern absorbs into the first rule -- .dsm-root was captured as "useShadowStyles(` .dsm-root" and silently dropped. That leaves one true finding, and it was real: .action-buttons padding-top was 12px as a Vue child and 4px in a shadow root. Both values date from the initial commit, so there is no later intent to infer; aligned to the shadow copy's 4px because that is the copy that reaches the shipped bundles, so it preserves what users have been seeing rather than changing it. check:synced-css:self-test poisons three properties in memory and requires each to be caught. inset is there deliberately -- it is the one the old implementation missed, so it is the case that proves the fix. Now in build:unlocked, which it could never be while it was red. |
||
|
|
1c01992a88 |
Gate the tab radius so a superseded rationale cannot quietly restore the pill
The 999px stadium was a real instruction that shipped, and its rationale is still in the layout file as history. A superseded rationale that still reads as current is how a reversed decision gets undone -- a reader trusting it would restore the pill believing they were fixing a regression. The comment now says not to; this makes that enforceable. It asserts the TOKEN rather than the value, because the requirement is that the tabs, the collapse button and the filter toggles agree. A shared knob guarantees that; three matching literals only coincidentally satisfy it. The expected fallback is READ FROM .vtl-collapse rather than hard-coded, so a gate asserting 8px cannot pass while the collapse button moves to 10px and the two surfaces silently diverge. Both synced copies are checked, so a fix applied to one host fails here rather than shipping correct in one and wrong in the other. .vtl-cap keeps its pill and is deliberately not asserted: the complaint was the ratio of radius to size, and a gate banning 999px everywhere would over-apply the correction. check:tab-radius:self-test poisons the source in memory and requires both regressions to be caught -- the pill restored, and a bare literal beside the token. Both caught, tree untouched. Wired into build:unlocked beside the other source-level checks so it fails in a second rather than after the vite loop. Noted while wiring: check:synced-css is NOT in the build chain, which is how two files have stayed in conflict. Left alone here -- turning it on is its own change with its own failures to fix first. |
||
|
|
2a9198fa3c |
Assert the shipped heartbeat ramp, not just the panel's fallback
check:heartbeat proved the PANEL FALLBACK was intact and called that "default
ramp unchanged". It is not the same property. The running app resolves the ramp
with dashboard.config.js's map.* layered on top of that fallback, and the gate
never read the real config: assertion F checked only that heartbeatThreshold was
DECLARED, never what it was SET TO.
Measured consequence, reproduced before fixing:
sed -i 's/heartbeatThreshold: 150,/heartbeatThreshold: 999,/' \
plugin-designs/research-dashboard/dashboard.config.js
node internal/check-heartbeat-threshold.mjs # exit 0
The shipped map could silently change its ramp while the gate reported it
unchanged. Now exits 1, naming the key and its value.
Adds assertion A2: resolve the ramp the way the running app does — panel
defaults with the shipped map.* applied — and require it to equal the historical
5/20/60/150/Infinity with its labels. A (fallback) and A2 (shipped) share one
comparison helper so they cannot drift. A2 also rejects a shipped heartbeatMode
other than 'fixed': a data-derived ramp is right for an unknown dataset and
wrong for this one, where 150 is deliberate. Changing the shipped ramp is now a
deliberate act that must update HISTORICAL in the same commit.
dashboard.config.js is read by brace-matching its `map:` literal rather than
import(): it pulls in @map/* aliases and a JSON module, so importing it would
need Vite's resolver — a dependency that buys nothing for a bag of scalars.
The self-test now runs three cases, each required to fire the assertion that
exists for it, and poisons in memory so a crash cannot leave the tree edited:
[literal] resolver replaced by the old array -> must fire B
[shipped-default] shipped heartbeatThreshold changed -> must fire A2
[shipped-mode] shipped heartbeatMode flipped to quantile -> must fire A2
The header previously claimed the gate was "negative-tested" on the strength of
the first case alone. True of that case, misleading overall, so it now states
per-case coverage and what is NOT covered (colour contrast, and whether the map
paints at all). Testing that the FEATURE works — a configured threshold moves
the edge — is not testing that the REGRESSION is caught, and only the second
protects the requirement that today's map must not change.
|
||
|
|
04c2a3d2eb |
Gate the rule that a facet selection must change the dots — and that the dots say what they mean
The prayer filter was reported as "not applying" to the map. Measuring it first
changed the fix: the filter was never broken. Selecting "Has Prayer" already
narrowed the rd-bubbles source 2107 -> 689 features, exactly matching the
dropdown's own tally, disjoint from "Needs Prayer" (1418), summing to 2107 and
round-tripping on clear.
What was broken is that circle-color was the flat literal '#ef6c00' — read back
off the live layer, the bare string, not an expression — so every dot painted the
same orange whether it had prayer or not. A prayer question got an answer with no
prayer information in it, and the surviving dots looked identical to the ones the
user started with. That is why a working filter reads as "not applying".
This gate asserts all three halves, because any one alone is a false pass:
1. the selection changes WHICH dots are on the map, by count AND by identity
2. every surviving dot satisfies the level that was selected
3. while a prayer filter is active the colour is a data-driven expression
reading peoplePraying, not a flat literal
Assertion 3 is the one that matters: 1 and 2 were already green while the bug was
visible. Identity is checked as well as count because the filter removes from the
tail of an alphabetical list, so the first five slugs are byte-identical before
and after — a gate reading the head of the list would report "unchanged".
NEGATIVE-TESTED in four modes, each isolating one assertion, all verified caught:
colour reverts only the paint (the reported bug) -> caught by assertion 3
props strips peoplePraying -> caught by assertion 2
frozen blocks the source update entirely -> caught by assertion 1
both reverts the lot -> caught
A gate that cannot fail is worse than no gate.
Verified on both hosts against the BUILT tree: the cap-5 shell and the standalone
leaf. Browser gate, so not in build:unlocked.
|
||
|
|
fa0b7d1099 |
Keep the development-era internal code name out of shipped files
The repo is handed to the Doxa team, and a code name from its build history
means nothing to an inheritor. Two places carried it, both regenerating:
- public/.build.lock was TRACKED. internal/build-lock.sh stamps it with
`by=$(whoami)` under flock on every build, so each rebuild committed the
building machine's username. Scrubbing the string would have been useless;
the file is pure runtime state nothing reads but the lock, so it is now
ignored via public/.gitignore instead.
- check-no-absolute-paths.mjs quoted a real home path in its header to
explain the rule. The prose works the same with /home/<user>/.
check:no-codename makes it stick. It scans tracked files only (the set a clone
receives), names every offending file:line in one pass, and refuses to pass
vacuously on a short file list.
Two things it has to get right or it becomes noise. "10x" is ordinary English
for a multiplier and library/scripts/research uses it correctly, so only the
full two-part name matches. And the same letters appear as the unix username
inside /home/ paths, which is a portability defect check:no-abs-paths already
reports better — those are listed, not failed, so the two gates cannot
disagree about one string.
It carries no ALLOW entry for itself. A gate must never be its own witness,
and an exemption is a hole a real leak could hide in, so the pattern is
assembled from fragments at run time and the file scans itself on the same
terms as every other.
Negative-tested: planting the name in README.md fails naming README.md:2,
exit 1; removing it passes. All five spellings (LK10X, LK 10X, lk10x, LK-10X,
lk_10x) planted across five different shipped surfaces are reported together
in one run, exit 1; reverting returns exit 0.
Also quotes check:heartbeat into the CLI doc, which check:quoted-scripts
requires and which landed in package.json without it.
|
||
|
|
04eccdfa7a |
Gate the scrim-covers-viewport rule, and say what was not verified
Some checks are pending
build-gate / build (push) Waiting to run
A `position:fixed; inset:0` scrim is only viewport-relative while nothing
establishes a containing block for fixed descendants. VerticalTabsLayout sets
`contain:layout` on .vtl-content, so inside the cap-5 studio every such scrim
covered the CONTENT PANE instead. Measured at a 1280x900 viewport:
.dt-scrim / .modal-scrim / .lt-overlay ... 1024x793 @ (256,107)
captured by div.vtl-content [contain:layout]
256 is the rail width, 107 the header height — left uncovered and CLICKABLE
behind what still looks like a modal. The fix (library/utils/viewportScrim.js)
measures the real origin and subtracts it, rather than removing the shell's
contain, which is load-bearing for a different reason.
This adds the gate that keeps it fixed. A source grep cannot: every one of these
scrims greps as a correct inset:0, because the variable is the ANCESTOR, not the
declaration — and which ancestors exist depends on the HOST, not the component.
check:scrim-viewport:self-test reverts each scrim to plain inset:0 and requires
EVERY case to fail; a gate that cannot fail is worse than no gate, and this one
measures a property that looks fine in a screenshot either way. The self-test
also surfaced that .lt-overlay is captured TWICE — div.lt-root [transform] as
well — collapsing it to 233x30 rather than merely to the pane.
SemanticTreeLegend's .stl-export-backdrop is fixed the same way but marked as
NOT verified through the UI: it sits behind SHOW_EXPORT_BUTTON = false, so the
menu that owns it cannot be opened. Correct-if-enabled, not observed.
|
||
|
|
ad6da54674 |
Checkpoint the swarm's in-flight work
Some checks are pending
build-gate / build (push) Waiting to run
Five lanes had 465 insertions across 18 files sitting uncommitted, some for 15 minutes. That is the state that silently lost the shell-scroll fix earlier today: a verified fix existed only in the working tree, a second agent's edit to the same file overwrote it, and the reverted version got committed and described as the fix. Nothing here is finished and no card is being closed on it. This is a snapshot so the work survives an overwrite, taken at a moment when every file had been quiet 15s or more, so no agent was mid-write. Verified coherent before staging rather than committed blind: template-literal, mjs-parse, sfc-parse, no-orphan-gates, kpi-unit, composite-reg, script-flags, quoted-scripts and backlog-status all PASS. check:synced-css fails on PeopleGroupDetail.vue and doxa-simple-map.vue only -- the same two files and the same counts as before the swarm started, both untouched by any agent, both already carded. Two untracked files included because both are load-bearing: internal/ check-facet-one-field.mjs is wired into package.json with a self-test, and library/utils/viewportScrim.js is imported by DataTable, SemanticTreeLegend and PosterDialog -- a shared helper for the modal-scrim fix rather than six copies of the same workaround. |
||
|
|
f00abe1e6c |
Parse the build scripts before the build runs them
Some checks are pending
build-gate / build (push) Waiting to run
Two different agents broke the build the same way within ten minutes: a backtick inside a // comment sitting inside a template literal of generated browser JS. The backtick closes the literal and the parser reads prose as code, so it fails hundreds of lines from the edit -- one break reported at :386 with the cause in a comment above it. check:template-literal did not catch either, and that is not a bug in it: it scans backticks inside CSS comments, the shadow-CSS shape. A // comment inside a generated-JS literal is a different grammar. Proven by controlled negative test -- injecting a backticked term into a // comment in generate-tree.mjs, a file that gate explicitly lists, left it reporting PASS while node --check failed immediately. The rule was right; the implementation covered a narrower case than the rule stated. Nothing parsed the 70 .mjs build scripts. check:parse covers inline scripts in built pages and check:sfc-parse covers .vue, so the scripts the build is made of were the one unchecked surface. Uses the parser rather than a wider regex. A backtick is legal inside a string, a regex, a nested template, or a comment inside a template; a pattern precise enough to catch the bad case would false-positive on the legal ones, and a gate that cries wolf gets skipped -- worse than the gap. The parser already knows all of that. Parses, never imports: these scripts read files, spawn browsers and hit the network at import time, and a syntax check must have no side effects. Runs as build step 2, beside check:template-literal, so a broken script fails in a second rather than after minutes of compilation. The failure message points at the real cause rather than the reported line, and tells the reader to grep for every instance in one pass -- node --check reports only the first, and each rebuild to surface the next costs minutes, which is how one of today's breaks was being fixed one at a time. Negative-tested side by side: exit 1 with "SyntaxError: Unexpected identifier 'TREE'" at generate-tree.mjs:369, while check:template-literal reports PASS on the same file in the same moment. |
||
|
|
b9a0cb040c |
Require a queryable status on every backlog card
Some checks are pending
build-gate / build (push) Waiting to run
Cards were being written with explanatory statuses -- "done — reconciled: …", "blocked — Steps 1+3 DONE and re-verified…", "ready". The prose is valuable and several of those strings carry measurements, but it made status unqueryable: every count used status !== 'done', which is FALSE for "done — reconciled: …", so finished cards kept counting as open. Measured across the three phase backlogs: 130 of 395 cards carried a prose status and 71 of those were already done. Reported open was 176 against a real 105. The P0 figure quoted every tick was 21; it was 14. That is worse than an off-by-N because a backlog is a decision instrument. It answers "what is left" and "is the rest human-gated", and inflating the open count makes a drained board look stalled, which pushes toward manufacturing work instead of holding honestly. The error also hides: every card reads correctly to a human, because the prose says exactly what is true. Only the aggregate is wrong. Migration preserved every explanation in fields.status_detail -- 130 of 130, none lost -- and normalised status to the enum. Six cards whose head-word was not in the map were left alone and resolved by hand against reality rather than guessed, since a wrong normalisation hides a card instead of counting it. The gate does not require status_detail. Most cards need no explanation and demanding one turns a real check into paperwork satisfied with filler. Negative-tested: restoring one prose status fails by name and prints it. |
||
|
|
e1baca1bbb |
Assert the cap-5 rail opens on the Big Picture, in the specified order
Some checks are pending
build-gate / build (push) Waiting to run
The Driver named the sequence: Big Picture, UPG-100, Prayer Progress, Gospel Resources. Tab 1 is the landing surface -- the first thing anyone opening the product SPA sees, and the opening step of the onboarding ramp. That ordering was held only by the order of entries in NAV_MODEL and by comments explaining why, with nothing failing if someone reordered them. Easy to break silently: NAV_MODEL is also where sections, badges and narrow-screen flags live, so it gets edited for reasons unrelated to ordering, and a row moved while adding a badge produces no error and a SPA that still works. It just opens somewhere else. That file's own comments record Big Picture once being painted twice in the rail, from a one-row section that re-declared a row below. Asserts the RENDERED rail, not the source array. Parsing NAV_MODEL was the first attempt and returned an empty list -- the entries do not carry the id shape I assumed, and a regex over a 300-line commented array is guesswork either way. The rail is what the user sees and what the Driver described, and a source parse could pass while a rendering bug shows something else. Order, not membership: checking that four names are present would pass on a rail that opens on Gospel Resources. Prefix-matched so a row may carry its subtitle. Negative-tested: swapping the first two expected entries fails by name and prints want versus got for every row. |
||
|
|
7afbb9187c |
Assert a filter trigger keeps its width when an option is selected
Some checks are pending
build-gate / build (push) Waiting to run
.filter-dropdown was flex:1 1 160px with min-width:160px and max-width:240px -- a range, not a width -- so flex-grow renegotiated the moment the selected label appeared. The row grew 960 to 1294px and triggers reached 240px. Not cosmetic. The selections row with Clear All sits underneath the filters, so a filter that grows on selection re-flows the row below it and the band stops being a fixed block. The stable width is what lets the centred stack hold still. Independently reproduced before gating rather than taken from the agent report: 168px across all six triggers, identical after selecting the longest option in each. The negative test then reproduced the broken widths [240,240,240,184,171,219] -- the same array the original report cited, which confirms that measurement was real. Asserts the observable outcome, not the CSS. A grep for --dx-filter-w would pass while a missing min-width:0 on the label still pushed the box open: as a flex item without it the label is floored at min-content and refuses to ellipsise. Both halves are needed, so the gate measures the width. Selecting the LONGEST option is load-bearing. A short option fits inside the old range too, so a probe that clicks the first option passes on a build that still has the bug. Two hosts, and an unmeasurable host fails rather than skips -- three defects this week were invisible standalone and appeared only in the shell. Negative-tested: restoring the flex range fails both hosts by name, naming the before and after arrays and the total growth. |
||
|
|
ca39d27088 |
Check a composite registers everything the leaf it bakes registers
Some checks are pending
build-gate / build (push) Waiting to run
In the cap-5 shell the Region donut, country bars and Place tree rendered
entirely grey while the identical build was correct standalone. A cap-4
composite imports the sibling component directly
(dashboard-plus-grid.vue:18), which takes the COMPONENT and skips
research-dashboard/index.js -- the file that actually assembles the bundle:
Pinia, i18n, colour strategies, customElements.define. The composite
re-declares all of it by hand, a manual copy nothing keeps in sync, which is
how the strategies leg was lost. That file already documented this bypass for
i18n and fixed only that one leg.
It is silent by construction: getColorStrategy() falls back to language-family
for an unregistered mode instead of throwing, so the result is a plausible
wrong picture -- grey charts that read as a data problem rather than a wiring
one. Found by reading an image; every number passed.
The gate compares the leaf's registerStrategies keys against the composite's.
A composite registering nothing while its leaf registers nothing is fine: the
rule is about drift between two lists meant to agree. big-picture also bakes a
sibling and registers zero strategies, and that is correct -- neither it nor
people-groups-grid references colorFor, getColorStrategy or mapbox. A first
pass called it a second instance; measuring usage showed it is not one.
Four rounds to get working, each a real bug found by testing the wrong half:
- pre-stripping comments deleted 2,953 of 4,102 characters of
research-dashboard/index.js, including the call being searched for, so the
gate claimed "no composite bakes a sibling that registers strategies" --
a confident claim about the repo derived from parsing nothing. Not stripping
is also correct: the call is located by requiring "(", so a mention in prose
cannot match.
- indexOf('registerStrategies') found the IMPORT line first and brace-matched
its own braces.
- a non-greedy brace regex stopped at the first inner "}" and the key pattern
missed quoted path keys.
Every fix was verified against dashboard-plus-grid, which survives stripping,
and never against research-dashboard, which does not. A comparison needs both
halves asserted before any fix is believed.
Negative-tested: dropping doxa-region from the composite fails by name and
prints both lists.
|
||
|
|
fd41b4a207 |
Check that a component's two CSS copies agree
Some checks are pending
build-gate / build (push) Waiting to run
A shared component declares the same selectors twice: once in a scoped <style> block for when it is imported as a Vue child, and once in a useShadowStyles literal for when it is mounted inside a shadow root, where scoped CSS does not reach. Both are load-bearing and the standing rule is that they stay identical. Nothing enforced it. library/components/PeopleGroupDetail.vue disagreed with itself on eleven properties, and the shadow copy is a coherent DARK theme under a light-only rule: .detail-header background white vs #16213e, .image-container #f5f5f5 vs #2d2d4e, and .people-group-name color #333 vs #fff -- white text, legible only on the dark background, so fixing the background alone would make the name invisible. Six importers; the dark hex reached four shipped bundles. It was invisible because it is a two-host bug: the component looks correct in whichever host you tested. Fourth of that shape this week. Three findings while building this, each of which had the gate reporting success about something it never compared: - a loose /<style[^>]*>/ matched the words "<style scoped>" inside a JS comment 43 characters before useShadowStyles( in SemanticTreeLegend.vue, then ran to the next real </style> and swallowed the whole shadow literal into the <style> text. Both maps read the same bytes, so that file could not fail on any of its 51 apparent shared selectors. Anchored to line start, since an SFC block starts at column 0. That also raised PeopleGroupDetail 8 -> 11. - assigning m[sel] = props let the last declaration win, discarding every property that appeared only in a base rule when a media query redeclared the selector. Rules are merged now. - comparing raw text reported comma-spacing as divergence: 5 findings for doxa-simple-map of which 2 were formatting. Compared as property maps, so the rule is about meaning. Negative-tested by change rather than by exit code, which with pre-existing failures proves nothing: fixing .detail-header takes PeopleGroupDetail 11 -> 10 and restoring it returns 11. Standalone rather than in build:unlocked, because it fails on live defects today. |
||
|
|
a11ec14852 |
Print each KPI percent sign once
Some checks are pending
build-gate / build (push) Waiting to run
The dashboard rendered "99% % UNREACHED" and "1% ADOPTED %". kpiText returns k.value unchanged for kind:'pct' and that value already carries its percent sign, while the labels in dashboard.config.js carried a second one. Each half is correct alone. A label reading '% unreached' is a reasonable label; a formatter returning a ready-formatted percentage is a reasonable formatter. Only the composition is wrong, and neither file mentions the other, which is why it was found by reading the rendered screenshot rather than by reading either source. check:kpi-unit asserts no kind:'pct' label contains a '%'. The value owns the unit because it is produced by the formatter that knows the number; the label says what is being measured. Parses the config as text rather than importing it -- the research-dashboard module graph reaches library/colors/_registry.js, which uses import.meta.glob and throws under plain node. Text parsing keeps it hermetic enough for build:unlocked. Comments are stripped first, because the config now documents this defect right above the fixed line and a naive scan fails on the explanation of the fix. It also fails when it finds no pct entries at all, rather than printing a green PASS: if the config is refactored so the parse stops matching, silence would read as success forever. Negative-tested: restoring either label fails by name with exit 1. |
||
|
|
68c1686cf5 |
Gate that map, legend and charts share one palette
Some checks are pending
build-gate / build (push) Waiting to run
The dashboard aggregations module used to hold two private palettes the map had never seen, and picked chart colours with CATEGORICAL[i % CATEGORICAL.length]. Two failures came out of that: near-miss regions (Africa #e74c3c in a chart, #ff2e2e on the map) and religion coloured by array index. No religion key existed in the private table, so every religion fell through to its position in a list sorted BY COUNT -- and counts move when you cross-filter, so a religion changed colour as you filtered. Islam and Hinduism swapped, Buddhism and Christianity swapped. That is already fixed: both private palettes deleted, every hex now read from the same strategy modules the map paints with. But the fix is a deletion plus a convention, and nothing enforces a convention. The next person adding a facet writes a lookup table because it is the obvious thing to do, and the divergence returns with no error and no crash -- just two surfaces disagreeing about what blue means. Reads the BUILT bundles rather than the source because the registry discovers strategies with import.meta.glob, which plain node cannot evaluate. That makes it a test of the glob too: if the registry ever silently discovers nothing, the bundles lose the palette and this fails, where a source-only check would still pass. Comments are stripped before scanning, because both consumer files describe this defect at length in their headers and a naive scan matches the documentation of the fix -- the same self-reference that made two gates report on their own prose this week. Negative-tested three ways: a reintroduced private palette, an [i % len] lookup, and corrupting four of ten religion hexes in a built bundle, which fails as a partial palette and names the missing hexes. |
||
|
|
b2fc0e4dda |
Make the shell-scroll gate runnable, and catch orphan gates
Some checks are pending
build-gate / build (push) Waiting to run
verify-shell-scroll.mjs was written and negative-tested, then left with no package.json entry and no importer, so nothing could ever run it again. It also defaulted to localhost:8899 -- an ad-hoc port from an earlier session -- so it failed with ERR_CONNECTION_REFUSED for anyone else and read as a broken gate rather than a missing server. Now check:shell-scroll, defaulting to BASE_URL or 127.0.0.1:4173 like every other browser gate, and listed in verify-browser-all's GATES array. check:no-orphan-gates fails any gate-shaped file in internal/ that nothing can invoke. The repo carded this once already; it recurs because writing a gate and registering one are separate acts and only the first feels like work. Not every unreferenced file is a lost gate, so the rule is not "everything must be wired": theme-flash-probe is a dated diagnostic for two fixed bugs, worth keeping as a reproduction recipe. It now carries an @one-shot tag saying so, which makes the author state which kind of thing they wrote. Two self-referential defects in the first version of that gate, both from reading evidence out of documentation: - it cited theme-flash-probe.mjs in its own header as an example, then grepped for importers, found itself, and declared the orphan reachable. The gate written to catch orphans made an orphan pass by mentioning it. A gate must never be its own witness, so it now excludes its own file. - it matched @one-shot anywhere in prose, including its own paragraph explaining the tag, and reported "1 tagged" when nothing had been tagged. The tag must now be a marker at line start. Negative-tested: removing check:shell-scroll from package.json fails by name. The scroll fix itself is NOT done. contain:layout and .vtl-host overflow-y are confirmed in the built bundle, and a real wheel gesture still moves the grid 0px with 2586px mounted and unreachable. The gate is what caught that; the diff looked correct. |