84 commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
3d3586b5f9 |
Commit the map check, so the browser path survives this flight
Every browser check written for this bundle lived in _qa-cycle/, which is gitignored: a dozen probes proving the pins render, the sheet opens with real links and the deep link filters the table — all of which disappear with the working tree. check:coverage-consistent guards the DATA and cannot see the map at all. The repo has carded orphaned gates twice; this is the same loss one step earlier, a gate that was never committed. It asserts, against the SHIPPED tree through the production-parity server: the element mounts and exposes its state handle (a stale-SRI leaf silently refuses to execute, which looks identical to a mount failure); every pin in the source is RENDERED, not merely loaded; a tap opens a group with a NAMED language; the table deep-link carries that group's own ISO code; and the gap filter leaves only tier 0. ★ IT TOOK THREE TRIES TO MAKE IT WORTH TRUSTING, and each failure is the point. FIRST it was FLAKY — 1 run in 3. Clause 4 waited a fixed 900ms after clicking the filter, but setFilter is applied asynchronously, so it queried mid-transition and still saw tier 1 and 2. It now waits for the map's own idle event. 5 of 5 runs clean. A gate that fails a third of the time on correct code is a gate that gets switched off. SECOND its doctrine check had a SAMPLE SIZE OF 1. It validated the two URLs of the one group it happened to open — 1 of 2,685 codes — so a jw.org link planted anywhere else passed cleanly. MEASURED: the injection did not fail the gate until the scan covered the whole index. It now checks all 3,550 links, and fails if the index carries none at all, because a scan over nothing passes vacuously. THIRD, adversarially tested end to end: with jw.org injected on code `tel` the gate exits 1 naming the publisher; restored, it exits 0; the file's md5 is unchanged either side. Registered in package.json and in verify-browser-all's GATES array — check:no-orphan-gates confirms it, and check:quoted-scripts passes. Without a server it SKIPS loudly with the command to start one, rather than passing quietly. |
||
|
|
c2046be7ec |
Gate the agreement between the surfaces, after finding the same drift three times
Three defects this flight were the same defect, and none of them threw. Every page rendered; only comparing two surfaces caught them: the country sheet listed 36 groups for Indonesia while the map drew 669 pins, because the sheet read a curated subset and the map read the pins; the resource table silently dropped 506 real resources across 377 languages, because its generator skipped any code no pin speaks — in a table titled "every link"; resources attached to `und` told 14 people groups there was Scripture in a language the record explicitly does not identify. A fourth is a matter of time, so the comparison becomes a gate rather than a habit. SIX CLAUSES, all static, 60ms, no browser and no server: no ISO reserved code carries a resource; every resourced code has a language name; the table's reach columns match an independent recount off the pins; every resourced code appears in the table; no table URL exists that the pin index does not hold; and each pin's tier agrees with its code's URLs. Each one is a defect that was actually shipped, not a hypothetical. ★ IT REFUSES TO PASS VACUOUSLY. Five of six clauses assert a NEGATIVE, which is exactly the shape that reads green against empty or mis-keyed input, so an empty corpus is a FAIL rather than a clean run, and a missing input SKIPS loudly with the command to fix it. ★ AND IT SELF-TESTS. `--poison` injects one defect per clause in memory and requires all six to fail. Clause D is included deliberately: it is the one that would have caught the 506 dropped resources, and the first version of this self-test left it unproven — the most valuable clause was the one clause never shown to fail. 6/6 now. Registered in package.json and in build:unlocked, because a gate nothing runs is not a gate — this repo has carded that twice. check:no-orphan-gates went from FAIL to PASS on registration, which is the check confirming itself. The CLI doc quotes the build chain verbatim, so it was updated to match: package.json is what runs, the doc is the copy. No product file changed, so nothing is rebuilt. check:quoted-scripts, check:doc-scripts, check:no-orphan-gates and check:mjs-parse all pass. |
||
|
|
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. |
||
|
|
8e4884469b |
Make advertised script flags actually reachable
Some checks are pending
build-gate / build (push) Waiting to run
bun run appends arguments to the command string, so a script ending in an inline # comment swallows them. Proven side by side: "node argtest.mjs" bun run _t1 --self-test -> ["--self-test"] "node argtest.mjs # a comment" bun run _t2 --self-test -> [] No error, no warning: the script runs in its default mode and exits 0. Four scripts advertised a flag that could never arrive, and two of them are --self-test. A --self-test IS the proof that a gate can fail, so running check:facet-cascade --self-test printed the ordinary PASS and exited 0 -- which reads as "the negative test passed" when the negative test never ran. The gate itself was fine: invoking node directly reverted the cascade and failed 13 assertions correctly, including the real symptom (switching to another language family leaves the map empty because the orphan still filters). Only the invocation was broken, so the mechanism that certifies a gate can fail was a gate certifying itself. Each flag now gets a dedicated entry that hardcodes it, which also makes the mode discoverable in bun run output instead of documented only in prose nobody greps. check:script-flags fails any script whose comment advertises a flag the command cannot receive. Negative-tested: adding such a comment back fails by name with exit 1. |
||
|
|
0296b03aa7 |
Bind local preview servers to loopback only
Some checks are pending
build-gate / build (push) Waiting to run
serve used --bind 0.0.0.0, and every browser gate starts it. A live audit of this box found nineteen publicly bound http.server processes on 4820-4970 plus one on 8899, all rooted at public/, with curl to the box's public IP returning 200. Nobody noticed, because a listening socket produces no output and no failure: the gate it served passed and the process outlived it. The standing rule "keep the box closed" lived in prose. The behaviour lived in a flag. Prose cannot constrain a flag, so the rule was never violated by a decision -- only by a default nobody re-read. Loopback is sufficient for the whole suite: verify-browser-all, check:a11y-button-names, check:nav-reachable and smoke:share-all already default to 127.0.0.1. The wide bind bought nothing. check:no-public-binds fails any script with an explicit all-interface bind, a bare vite --host, or an http.server with no --bind at all (its default is also all interfaces, just less visible in review). It reads the repo rather than the machine's live sockets, because a socket check would pass or fail on whatever happens to be running and could not be fixed by editing anything here. Negative-tested: restoring --bind 0.0.0.0 fails by name with exit 1. |
||
|
|
a1005869ce |
Anchor the filter dropdown in every host, not just standalone
Some checks are pending
build-gate / build (push) Waiting to run
The menu is position:fixed with viewport coordinates. That resolves against the
viewport only while nothing establishes a containing block for fixed
descendants. VerticalTabsLayout -- the cap-5 shell that hosts every plugin as a
tab -- sets contain:layout paint on .vtl-content, inside .vtl-main{left:256px}.
Measured at 1600x900, same component, two hosts:
standalone research-dashboard ...... dLeft=0 dTop=4 six of six
inside plugin-studio dashboard tab . dLeft=256 dTop=111 every one
256 is the rail width and 111 the header height. Reported as the menu landing
"in a random spot further down to the right"; it was never random.
An earlier probe measured six triggers at four viewport widths and passed, so
the card was closed while the bug was still there. Every number was accurate.
They were answers about a surface the user never looks at -- widening across
more widths could not have found it, because the variable was the host.
Fix measures the containing block instead of assuming it: a fixed probe at 0,0
inserted in the menu's own parent reports the origin the menu resolves against.
When nothing captures, that origin is 0,0 and the standalone numbers are
unchanged bit-for-bit.
Not fixed by dropping contain from the layout: it is load-bearing there,
stopping a rail collapse from reflowing a heavy mounted map.
check:fixed-anchoring measures both hosts and treats an unmeasured host as a
failure. Negative-tested: reverting the subtraction fails the studio host with
dLeft=256 while standalone still passes -- it fails exactly where the old probe
passed.
Also cards a second symptom of the same cause, not yet fixed: fixed-position
modal scrims written inset:0 stretch to the content pane instead of the
viewport, leaving the rail clickable behind an open modal.
|
||
|
|
e1d27cb476 |
Record which kind of verification closed each card
Some checks are pending
build-gate / build (push) Waiting to run
A card was closed on an agent measurement while the bug was still visible to the Driver, and the card had no field in which that disagreement could live. The board said done and the product said broken, and nothing in the data knew. Adds fields.verification (agent|human|both) plus verification_kind (visual|state) and a conflict flag, back-filled across 179 closed cards, and a gate that refuses an anonymous close. The gate does not require human sign-off. Gating the board on human review was tried and it stalled. What it forbids is losing the distinction, so the question "which of these did a human actually look at?" stays answerable. The visual/state split encodes the asymmetry the Driver named: human verification is more reliable on visual bugs, agent verification on non-visual ones. A headless probe reads geometry, not perception -- it can measure a menu at dTop=4 and still be blind to it feeling wrong. So a visual card closed on agent evidence is a hypothesis; a state card closed on agent evidence is a result. The counts are reported separately. Negative-tested three ways: stripping a tag, claiming human verification with no quote, and marking a conflict card done each exit 1 by name. |
||
|
|
d79f01590f |
Put the CSV header on the first line so the export opens as a table
Some checks are pending
build-gate / build (push) Waiting to run
The export carried its provenance comment above the header, and every spreadsheet importer treats line one as the header — so the scope sentence became the column names and the real header became the first data row. A single-column line above a sixty-eight column table is what "glitched" looks like on screen. Three other explanations were plausible and all three are wrong, measured before anything changed: the file is comma-separated, not tab-separated, so the tabs in the report were terminal rendering rather than bytes; the escaper is genuinely RFC-4180, and embedded commas, quotes and newlines all survive a strict parser round trip; and injection-prefixed cells were already neutralised. Only the position was wrong, and chasing the other three would have produced churn and no fix. The provenance line moves to the end rather than away. It records which filters produced the file, which is the difference between a dataset and an anecdote, and a trailing single-column row lands as one populated cell instead of destroying the column mapping. The field counter that read line two moved with it — left alone it would have counted the first data row, usually the same number, so it would have looked right and been wrong. A new check parses the export the way a spreadsheet would, which no existing gate did: forty-six checks and not one read the file. It extracts the shipped escaper rather than restating the rule, so it cannot agree with itself while the product disagrees. Negative-tested by putting the comment back on top. |
||
|
|
950d3dff7f |
Catch a plugin the manifest forgets, without calling a gated one a defect
A bundle can be compiled and present on disk while its profiles never reach the manifest, and nothing noticed: the leaf floor counts index.html recursively, so one bundle losing every leaf while another keeps extras stays above the floor; the embed-target check iterates what the manifest lists, so an omission is invisible to it; and a bundle count passes too, because one bundle can be swapped for another and the totals still match. Counting is not covering. The only way to see an omission is to compare the manifest against the scanner rather than against itself. The first version of this check cried wolf on its first real run. It reported key-cities as missing — discovered with four profiles, absent from the manifest, and still absent after a full rebuild, which made it look persistent and real. It is neither: the build config gates that bundle by name and generate-tree filters on it, so the bundle is legitimately discovered and legitimately unpublished. The scanner answers what exists; the config answers what ships. Asserting the first against the manifest without consulting the second asserts an invariant that was never true. That failure mattered more than the bug it was chasing. A gate that reports a deliberate configuration as a defect gets switched off, and a switched-off gate protects nothing — so the check now reads the same config the build reads, and names the gated bundles in its passing output so the exclusion stays visible rather than becoming folklore. A manifest entry the scanner does not return is reported as a note rather than a failure: stale build output heals on the next full build, while a missing plugin is silently absent from the studio for everyone reviewing the product. Negative-tested: removing any published bundle from the manifest fails with exit 1 naming it; restoring passes; the gated bundle never trips it. |
||
|
|
f3cdc1ad8d |
A five-second studio check, so a dev-surface regression cannot ride along
Some checks are pending
build-gate / build (push) Waiting to run
One commit shipped the requested change and a regression together: a base prefix applied to an already-rooted URL 404'd all 29 studio tabs, and finding it took 32 minutes. The root-cause analysis attributes the largest single share of that to there being no gate that exercises the dev server at all — of 46 gate-ish scripts and 13 browser gates, none did. The built manifest hands relative refs and the dev manifest hands rooted ones, so a base-join bug is invisible to every build-time gate and fatal to every developer. The check finishes in about four seconds against an already-running dev server, which is the point: the full sweep costs about seventy-five, so it only ever gets run at the end of a change, which is exactly how a regression rides along unnoticed. It activates one tab rather than twenty-nine because a single mount already exercises the whole resolver path — manifest fetch, script resolve, module load, define, mount. If the resolver is wrong, the first tab fails like all of them. It asserts the five things that have actually broken: that the root serves the studio and not a staging page, that the element both mounts and registers, that the tab list is not empty, that no request failed, and that the activated pane holds real content — because an element can register and attach while its content never arrives, which is what a missing API base looks like. An unreachable dev server exits 2 rather than 1. Conflating "no server" with "broken product" is how a gate earns a reputation for flakiness and gets switched off. Negative-tested: removing the tree base makes it fail on the empty tab list, and an unreachable URL exits 2; restoring returns to green. |
||
|
|
1269ac5a52 |
Gate the URL-resolver rule the build could never have caught
Some checks are pending
build-gate / build (push) Waiting to run
Prefixing a base onto an already-rooted URL 404'd all 29 studio tabs, and no existing gate could have seen it: the built manifest hands relative refs, so concatenation is correct there and the assumption is never exercised. Only the dev server hands rooted refs, so the entire build-time suite passes green while every developer's studio is broken. A product that has to work on ten laptops cannot be verified by the build alone. The check discovers every shell that defines a resolve(ref) rather than naming them, because the two capability-5 shells carried byte-identical implementations and one latent assumption therefore produced two separate outages that had to be found separately. A third shell copied from either is now caught on its first run. It evaluates the shipped function against a stubbed host instead of re-implementing the rule, so the gate cannot quietly agree with itself while the product disagrees. Negative-tested: removing the rooted-ref guard fails 4 of 8 cases and reproduces the exact double-slash path from the outage; restoring it returns to green. |
||
|
|
b12f152944 |
Catch the live-CSS backtick by parsing, not by guessing
Some checks are pending
build-gate / build (push) Waiting to run
A backtick in live shadow CSS breaks the build exactly as one in a CSS comment does, and check:template-literal only reads the comments. Proven both directions last tick: the planted defect left that gate at pass while the build died with a Babel stack trace pointing far from the edit. The card concluded this was not achievable, on good evidence — three lookahead heuristics were tried and reverted, scoring 98 and then 136 false positives on a healthy tree, because the real terminator of a template literal is also a backtick and no window of surrounding characters separates the two. The evidence was right and the inference one step too strong. No heuristic works is not nothing works, and the card named the answer in its last line: needs a tokenizer. A parser does not guess where a literal ends; ending it is what parsing means. Measured against the same tree the heuristics failed on: zero false positives across 83 files. Splitting the SFC first means it sees exactly the script text the build compiles, and using the parser the build reports errors from means its verdict matches what actually breaks rather than approximating it. Negative-tested with the exact case the other gate misses: a backtick planted in live CSS passes check:template-literal and fails this one, naming the file and the line that was edited rather than a stack trace somewhere else. That line number is the point — the build was always the backstop, just an unhelpful one. |
||
|
|
f132d2e056 |
Put share verification back in the browser suite
Some checks are pending
build-gate / build (push) Waiting to run
I excluded smoke:share from the aggregate two ticks ago and the reason was wrong. Every clause of it was true — the script takes required arguments, and its gallery mode does need the vite dev server — but the conclusion did not follow. I had tried exactly two invocations, bare and --plugin-gallery, and never --url, which the script's own header documents first. It runs fine against the static tree: 8 of 8 pass, each Share button reopening the correct custom element with a live canvas. That is the same shape I logged as an anti-pattern last tick, now found in my own work: a not-applicable verdict is more durable than a wrong bug report, because it closes the question. It cost the suite its highest-consequence surface for two ticks. A share URL is the one artefact a partner pastes into an email, and it fails silently. It runs as smoke:share-all, carrying its own eight --url flags and --browser chromium, since Firefox headless has no WebGL on this box and the default engine stops. The eight leaves are the set derived by grepping for ShareButton, not a guess. The simple-map case is the discriminating one: it shares to a different leaf than the one opened, so the deep link is really being followed rather than an SPA fallback rendering something plausible. Two shell traps on the way. A VAR=x prefix does not apply to $VAR on the same line, because the shell expands the argument before the assignment scopes to the command, so the URLs came out with an empty base; deferring through sh -c fixes it. And a glob in a block comment closed the comment early. |
||
|
|
f3a5b228ee |
Make the flagship capability-5 screenshot show the studio doing something
Some checks are pending
build-gate / build (push) Waiting to run
It showed an empty pane reading "Choose an element from the list." The README's headline image for the highest capability level demonstrated nothing, and every shot gate passed it, because freshness, distinctness and element counts are all satisfied by an empty pane. The shot now selects the first rail row before capturing, so the picture shows what the studio is for: a nested SPA rendering inside the shell that lists every element by capability. The file went from 167KB to just over a megabyte, in line with its sibling. Found by reading the image, the same way the mobile error card was found yesterday. Reading one picture has now caught two defects that eleven gates passed. Add check:shot-fonts. The studio labels each row with a capability emoji, and this machine has no emoji-capable font, so all 28 rendered as tofu boxes in the committed PNG. The markup is right and the product is fine in a real browser — the shot machine could not draw it — so the fix is not to strip the emoji. The gate asks fontconfig whether this system can render the glyphs the UI actually uses, and names the font install rather than the markup. It is deliberately not in build:unlocked: it reports an environment condition that is currently true here, and a gate that always fails in the build makes the build useless. Its first version also flagged the star this repo uses as a comment marker, which every text font renders; the range is now narrowed to the pictographic planes. |
||
|
|
d463cb6161 |
Run every browser gate from one command
Some checks are pending
build-gate / build (push) Waiting to run
All seven browser gates were orphans: each had a script, and not one was invoked by anything else. They were reachable only by someone who already knew the name to type, which is exactly the person who does not need the list. This repo has been bitten by this before — the Gate-1 acceptance test existed, worked, and was invoked by nothing, so it could never report anything. An orphan gate is worse than a missing one because it looks like coverage. The data guards already solved this; verify-all.mjs says it plainly — a guard nobody runs is documentation. That file has no reference to playwright or to the built tree. It is the data half. This is the browser half. Every gate listed here was run before it was listed, following the rule the earlier audit set: listing a program you have not executed puts unvalidated entries in the hand-off surface. All six pass, and the aggregate now runs them end to end: capabilities, embeds, map paint, the Gate-1 chain, accessible names, and phone navigation. smoke:share is excluded on purpose and says so. It takes required arguments and its gallery mode reads a registry only the vite dev server serves, so listing it would make the suite fail for something that is not a defect. Preconditions are asserted up front and exit 2, so a missing server reads as a setup problem rather than six mystery failures. |
||
|
|
498bf9409c |
Gate that a phone can actually open the tab rail
Some checks are pending
build-gate / build (push) Waiting to run
Yesterday the cap-5 shell shipped unusable on a phone: the rail was an off-canvas drawer, the toggle lived inside a header the shell does not render, and the only other control sat inside the drawer itself at x:-244. Every tab but the one that happened to open was unreachable, with no error and a page that painted fine. That survived because it is an intersection bug. Header-off is correct, and an off-canvas mobile drawer is correct; the defect exists only where they overlap. So this checks the intersection directly — each cap-5 shell as it ships, at a phone viewport — and asks whether any on-screen control opens the rail. It then presses that control and requires the rail to arrive. Presence is not function: a present-but-inert opener is the same dead end wearing a disguise, and asserting only presence is how the original bug survived a card marked verified. This is not covered by the accessible-name gate. Neither property implies the other: the off-canvas button had a perfectly good name while being unreachable, and reachability is geometry, not markup, so no source grep can answer it. Negative-tested by re-creating the real bug — removing the floating opener makes it fail naming that shell, while the header-bearing studio still passes. |
||
|
|
70df4f00f6 |
Gate that every shipped leaf carries its own map token
Some checks are pending
build-gate / build (push) Waiting to run
A cap-5 shell nests its children as iframes, and window.MAP_TOKEN does not cross an iframe boundary — each frame is its own window. A nested map cannot inherit the parent's token, so it works only because the child leaf carries its own baked copy. Proven in a browser: all thirteen nested frames in the cap-5 shell hold a real token, including a map two levels deep. That was a one-off run; this makes it standing. The existing smokes cannot catch this. Both inject window.MAP_TOKEN with addInitScript before the page runs, which is right for what they test but means the test supplies the thing under test. A user opening a shipped leaf has nothing injecting a token for them. Also asserts the reverse: no token may be compiled into a .js bundle. Bundles are cached, SRI-pinned and reused across hosts, so a token baked into one cannot be rotated without a rebuild and leaks into every embed. Reads the built manifest rather than walking directories, so it checks the set that actually ships. Counted 43 leaves, matching an independent filesystem count. Negative-tested both directions: stripping a leaf's token and planting one in a bundle each fail with the artifact named. |
||
|
|
b57aef457c |
Add the mobile capability-4 composite: research map above the people-groups grid
Some checks are pending
build-gate / build (push) Waiting to run
The desktop composite pairs the research dashboard with the grid. This is its mobile sibling: the monolithic 7-tab research map on top, the same grid beneath, stacked to scroll. Which one a shell shows is an aspect-ratio choice made at the SPA level, so neither plugin knows about the other and both stay embeddable. The grid's own search and filters are ON here. The desktop composite turns them off because the dashboard header owns those controls and drives the grid through props; the research map has no such header, so switching them off would leave the grid with no way to be searched at all. Composes its children by tag and bundle URL rather than importing their .vue files. Importing the research map directly would bypass its bundle entry, which is where its three bundle-private colour strategies, its profile registry, its Pinia and i18n pair and the RTL text plugin are installed — and losing those is a runtime defect in a green build, because a thrown setup() removes the subtree with an empty error. Add check:composite-srcs, which resolves every nested src against the built tree and asserts every nested tag is registered. A wrong relative depth is a 404, and the stack layout swallows a failed section on purpose so one broken child cannot take the whole stack down — which means the cost of that resilience is a silently empty band. Negative-tested both ways: breaking the depth and renaming a tag each fail with the file and path named; restoring returns to pass. |
||
|
|
ec93f8aa1a |
Gate: every declared dependency must be a real package name
Some checks are pending
build-gate / build (push) Waiting to run
Makes the cheap half of last tick's P0 automatic. A folder rename had rewritten three npm package names, so bun install failed in a fresh clone while everything here passed -- the real packages were already in node_modules from before the rename, making the breakage invisible to every build and every gate. Checks two things, offline, in milliseconds: the declared name resolves under node_modules/, AND the installed package's own name field equals the declared key. The first alone catches the capacitor case; the second catches a name that resolves to a DIFFERENT package, which a path-only check would miss. Together they detect the corrupted-name class without a network round-trip, which is what lets it run on every build rather than in a nightly job nobody watches. It does not replace the clone test. A gate can only see what is IN the repo; only a clone sees what the repo fails to do on a clean machine. Negative-tested four ways: recreating the EXACT P0 (the three @deployment/capacitor/* names) FAILS naming all three; a name that resolves to a package whose own name differs FAILS; an absent node_modules SKIPs honestly rather than passing or failing; an empty dependency list FAILS via the vacuous guard. Wired into build:unlocked and confirmed running inside a full build (17 declared, 17 verified, exit 0). |
||
|
|
16834041b6 |
Fix three dependency names a folder rename corrupted: bun install was broken in a clone
Some checks are pending
build-gate / build (push) Waiting to run
Tested the north-star path as an inheritor would, by cloning the repo from Forgejo and running the documented first command. It FAILED: error: GET https://registry.npmjs.org/@deployment%2fcapacitor%2fcli - 405 error: @deployment/capacitor/cli@7 failed to resolve The rename of capacitor/ to deployment/capacitor/ did a blind string replace that rewrote the NPM PACKAGE NAMES too. @capacitor/android, @capacitor/cli and @capacitor/core became @deployment/capacitor/*, which do not exist on npm. Restored the real names, pinned to ^7.6.9 -- the version actually installed -- and fixed the two places that repeated the wrong name in prose. This is the inheritor-only failure shape, in a form the new absolute-path gate cannot see. The local tree works perfectly because node_modules/ @capacitor/* was installed BEFORE the rename, so every build and every gate passes here. The breakage lived only in the DECLARED names, visible solely to someone installing from scratch. Verified by doing it: a fresh clone now runs bun install to exit 0 (206 packages, all three @capacitor/* resolved) and bun run build to exit 0, producing 18 bundles / 27 profiles -- identical to the working tree. |
||
|
|
8fcb187f36 |
Gate: no machine-specific path may enter tracked source
Some checks are pending
build-gate / build (push) Waiting to run
The repo is clean -- git grep over every tracked file returns zero /home/ paths. That is a real hand-off property: a clone works on any machine, which is what "the path Doxa inherits" requires. But nothing enforced it. Proven by experiment: a /home/lk10x/... line planted in internal/settled-tree-gate.mjs passed check:public-hygiene, check:parse AND check:quoted-scripts. The tree was clean by accident, not by construction. This is the worst failure shape for a hand-off. An absolute path works perfectly for whoever wrote it and breaks only on the inheritor's machine, so no test run by its author can catch it. The sibling ideation tree already paid that cost: two docs cite a workspace root that no longer exists, so a reader following them silently finds nothing. Not a flag on check:public-hygiene, which already detects these patterns but walks a SERVED tree where node_modules never exists -- pointed at the repo root it produced 1,427 findings, nearly all from node_modules/.vite. Its scope is right for its job. This scans `git ls-files`, the set a clone actually receives, which excludes node_modules and build output with no ignore list to maintain. Negative-tested three ways: the exact planted path that beat three other gates now FAILS with file:line; a /Users/ path in a doc FAILS; a checkout with too few tracked files FAILS rather than passing vacuously. Wired into build:unlocked and confirmed running inside a full build (783 files, exit 0). It then caught its own documentation -- my reference.md row quoted the literal patterns as examples. Rewrote the example rather than allow-listing the file, which would have blinded the gate to a real path in the repo's largest doc. |
||
|
|
e2c5c2ec49 |
Gate: every shipped profile must say what it is
Some checks are pending
build-gate / build (push) Waiting to run
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. |
||
|
|
9dc4dda8cf |
consolidate docs into documentation/ and deployment shells into deployment/
Some checks are pending
build-gate / build (push) Waiting to run
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.
|
||
|
|
0c7bfb9b9c |
add a catalog CURRENCY gate: is the live tree the build we have?
Some checks are pending
build-gate / build (push) Waiting to run
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. |
||
|
|
ea53a2e19a |
emit a real desktop binary and .deb for deploy method 5
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. |