259 commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
4eec165a2b |
Merge remote-tracking branch 'origin/main' into shepherd/merge-b3
Some checks are pending
build-gate / build (push) Waiting to run
|
||
|
|
64fed43449 |
packet: stamp how a packet was built, into the packet
Some checks are pending
build-gate / build (push) Waiting to run
out/packets/ is gitignored, so check:packets-all grades whatever .html files happen to be sitting there — an accidental set, not a current one. On this machine that was 11 freshly generated packets. In doxa-maps-plugin-studio, which shares main with this worktree, it was two NOPHOTOS=1 layout probes of mine from earlier ticks, and the sweep was RED there for anyone who ran the wave. The gate was honest about each file it checked; the set it checked was accidental. The underlying problem is that a fast layout-iteration build and a shippable one were byte-indistinguishable. The packet carried lang and dir and nothing about its own provenance, so the only way to tell them apart was for the person running the gate to remember and pass PACKET_EXPECT_PHOTOS=0. packet.mjs now stamps data-packet-build with the flags it generated under, and the gate reads it. The GENERATOR writes it rather than the gate inferring it: had the gate guessed "no photos present, so probably a probe", it would have excused genuine photo-fetch failures. Tested explicitly — a packet stamped "full" whose map svg is then stripped STILL FAILS. The stamp records intent and cannot excuse a defect the build did not declare. Deliberately not a timestamp. An mtime staleness rule was considered and rejected on evidence from this same session: packet-strings.json was 303s newer than the packets while byte-identical to HEAD, because a probe had restored it from backup, so an mtime rule would have failed a correct tree — the exact mistake fixed two commits ago in the library-index gate. Content comparison is not available here (regenerating hits a live API and takes minutes), so the build records its intent instead of the clock. The studio sweep goes from "2 checked, 0 passed" to "1 checked, 1 passed", with the packet self-declaring build="nophotos nomap". Env vars stay as an override for pre-stamp artefacts, and the declared-deliberate note now names whichever source actually declared it rather than always crediting the env var. All 11 locales stamp "full". i18n re-verified with a partial block (1 string, 3/12 points, 6/12 rhythms): wheel 6 translated and 6 English, 12 point rows with 6 translated, 0 blanks. Wave 34/34, packets-all 11/11 here and 1/1 in the studio. |
||
|
|
445131cf94 |
Merge main into bundler-3/work — settle the 21-file conflict, keep both campaigns
Only 2 files conflicted in source: package.json and its CLI doc, both taken to main's refactored gate pipeline (gates:pre/gates:post/build:bundles) after verifying every check the old inline chain named still runs there. The other 19 were generated: public/assets/tables mirrors keep main's deliberate deletion, and public/doxa-maps is regenerated by the build that follows. |
||
|
|
588d0da48a |
packet gate: look inside the PDF it was already making
Some checks are pending
build-gate / build (push) Waiting to run
check-packet-render has called page.pdf() for a long time and then read only
MediaBox and /Type /Page. Eight blank sheets of the right size would have passed
every assertion in it.
That matters because the packet exists to be PRINTED — it is a standalone file
with photos and fonts inlined precisely so it prints offline — and until now
every check was on-screen. A print-only CSS rule can strip content that the
on-screen checks still see.
The first mutation is the argument for the whole change. Adding
`@media print{.card img{display:none}}` left the on-screen line reading
"95 photos, 0 typographic" — every pre-existing check green — while the PDF
carried 2 images instead of 85. The second, hiding sheet contents in print,
fires all three new assertions at once on a packet that is still 8 correctly
sized pages.
Images are compared against DISTINCT srcs, not cards. A PDF de-duplicates
identical images, and laos has 95 cards and 95 <img> but only 85 distinct
sources, because ten groups share a portrait — a fact about the upstream photo
set, not a rendering fault. Comparing against 95 would have reported "8 photos
dropped at print time" on a correct packet. I nearly did exactly that.
A scare investigated and cleared rather than reported: the PDF names only
DejaVuSerif, which looked like the vendored Devanagari face failing to reach
print. It is not. Under print media emulation the stack still resolves to
"Noto Serif Devanagari", and the same string measures 210px in it against 420px
in plain serif, so the face is genuinely in use and the PDF names are Chromium
subset aliases. Corroborated independently: hi is the largest packet at 3.2MB
with the fewest faces at 7, which is one big vendored face, not a fallback. A
ToUnicode-based check was discarded after calibration found only 30 Latin
codepoints in a known-good English PDF — an incomplete parser whose
zero-Devanagari result proved nothing.
All 11 locales embed fonts and carry every photo. i18n re-verified through the
new assertions with a partial block (2 strings, 4/12 points, 5/12 rhythms):
2.8MB, 11 fonts, 87 images, wheel 5 translated and 7 English, 12 point rows,
0 blanks. The studio's declared map-less photo-less packet still passes — the
image assertion skips at 0 distinct sources, so the opt-outs compose. Wave 34/34.
|
||
|
|
e0fd61eb3e |
Gospel presentations in 94 languages, from Cru's API rather than its parked domain
Every pool here so far is Scripture, film, Bible video or a course. None is a GOSPEL PRESENTATION — the short translated thing a person actually opens with someone. Cru publishes those, in the same organisation that makes the JESUS Film, so the doctrine question is settled the same way the brief settles it for the film. Only their gospel tracts are included, from a fixed allow-list; the lessons and survey tools are not. IT CLOSES NO GAP AND SAYS SO. All 94 of these languages already have something, so this is depth rather than coverage and moves no tier. Tiers are byte-identical at 2901/4767/4711. It reaches 3,270 pins and 3.97bn people. godtools.org, the obvious host, IS A PARKED DOMAIN. It answers 200 to everything with a 114-byte body, and rendered, every URL — including /zz/nosuchtool-xyzzy — reads "godtools.org is parked free, courtesy of GoDaddy.com". Shipping links there would have sent people to a GoDaddy ad. The real front-end is knowgod.com, and it does serve per-language tracts: /en/kgp reads "Knowing God Personally", /sw/kgp reads "Kumjua Mungu Kibinafsi". But ONLY once rendered. By fetch it is a uniform 3,864-byte shell for every URL valid or not — the third host in this bundle with that shape, after jesusfilm's old ?languageId= and live.bible.is. So existence is proved from Cru's own API and never from the page: a (language, tool) pair exists exactly when the resource's latest-translations intersect that language's translations. Checked against the browser before being trusted — en/kgp and sw/kgp both intersect and both render real localised text, and the bogus pair does not intersect. In band, the verifier's controls are now 7 negative all non-verified and 7 positive all verified, with a bogus knowgod pair among the negatives. All 94 URLs verified. Clause I keeps its closed set: Gospel presentation only on knowgod.com, and knowgod.com only that type. Poison self-test still 12/12. Screenshot read: German shows six distinct resource types, including "Gospel presentation - Knowing God Personally". |
||
|
|
d561e35d33 |
packet gate: a map one-pager with no map on it is not a map one-pager
Some checks are pending
build-gate / build (push) Waiting to run
Deleting the map svg from page 1 entirely left this gate reporting
"map: palette resolves, not black — PASS, map painted".
mapFaults queried `document.querySelector('.sheet svg')` — the first svg
ANYWHERE in the document. With page 1 empty the query silently retargeted to the
PRAYER WHEEL's svg two sheets later, and the wheel has no CSS variables to leave
unresolved and no black fills, so both checks passed vacuously. The sheet count
stayed 8 because the empty sheet still exists.
A fallback selector turns an absence into a pass: when the thing being asserted
about disappears, the query finds a different object that happens to satisfy the
checks — and here the gate printed a stronger claim than it had evidence for.
"page 1 = map one-pager" is the first line of the packet spec.
Now scoped to sheet 0, which the page order fixes as the map page. And it
asserts the map paints GEOGRAPHY, because palette-resolves and not-black are
both properties a map with zero country shapes satisfies perfectly. Geography is
paths whose `d` exceeds 200 chars — a coastline, not an icon. The floor of 5 is
set from a measured spread rather than guessed: laos 13, brazil 18,
middle-east-core 55, tibet 69, uncovered-scatter 74, varying with how many
countries a zone spans.
Mutation-tested in three shapes: no svg fails naming page 1; an svg present but
drawing nothing fails at "only 0 geography path(s)"; the good packet passes at
13. All 11 locales report 13 — the map is data, not text.
Syncing the gate surfaced a second thing. doxa-maps-plugin-studio ships 0 poster
html files against wt-packet's 28, and packet.mjs deliberately "still builds
(cover page without the map) rather than failing" when out/posters/<zone>.html is
absent. That soft-fail is intentional but it was invisible: the degraded packet
has 8 sheets, an <svg> present (the wheel's), and no marker at all, so it is
structurally indistinguishable from a good one while page 1 promises a map and
delivers an empty sheet. PACKET_EXPECT_MAP=0 follows the PACKET_EXPECT_PHOTOS
precedent — fail by default with the remedy named, pass when the degradation is
declared.
Also fixed the summary line, which read "map painted" unconditionally and so
described a declared map-less packet as having a painted map. It now reads
"NO map (declared)". The summary is the part a reader quotes.
i18n verified with a 2-key partial block: page 1 renders the translated keys,
falls back to English elsewhere, 0 blank runs. Wave 34/34, packets-all 11/11.
|
||
|
|
6c60af555f |
library-index gate: ask whether the menu drifted, not whether a file is older
Some checks are pending
build-gate / build (push) Waiting to run
`touch library/components/legends/SemanticTreeLegend.vue` — zero content change, nothing in git status — turned this gate red. So did a git checkout, a rebase, and any editor that rewrites a file on save. It cried wolf twice in one build session while the regenerated doc was byte-identical both times, and the only available response was to run the generator and commit nothing. An mtime answers a different question than the gate's own name asks. This is the SECOND time this exact class was fixed here. The first was a SCOPE error: the gate walked 2.3x more of library/ than the generator reads, so a comment in a build script turned it red and named that script as the reason the component MENU was stale. Narrowing the scope fixed that instance, but the mechanism was still mtime, so the class survived inside the narrowed scope. The lesson recorded then applied again word for word — a gate that fails for a change it can prove is irrelevant teaches the reader to regenerate on reflex, which is how a staleness check stops being read. It now regenerates into a throwaway tree and diffs the bytes. generate-library-index.mjs hardcodes its output path and derives ROOT from its own location, so it cannot be told to write elsewhere — but copying the SCRIPT into a temp tree whose library/ is a SYMLINK to the real one relocates its ROOT without modifying it. The generator runs unmodified against real sources, writes somewhere disposable, and an interrupted run leaves nothing behind. Verified byte-identical to a real `bun run library-index` before this was written, and the generator was confirmed deterministic first — a content diff would be useless otherwise. Comparing output also makes the scope self-enforcing: a change outside components/, composables/ and utils/ cannot alter one byte of the menu, so the INDEXED_DIRS list the mtime walk needed is gone. Mutation-tested both ways, because a gate that never fires is worse than a noisy one. Deleting a catalogued row fails with the line number and expected content. Adding a real new library component fails — that is the drift this exists to catch. Touching 40 library files, the rebase case, passes. Wave 34/34. Swept the five other internal/*.mjs that reference mtime: only check-shot-freshness also decides on one, and there it is correct — a PNG cannot be cheaply regenerated and diffed, and a rebuilt bundle is meaningfully newer. Left alone deliberately. |
||
|
|
53fea6ee38 |
packet gate: a prayer wheel with no words on it is not a prayer wheel
Some checks are pending
build-gate / build (push) Waiting to run
Blanking every wheel segment label and regenerating left check:packet-render reporting "12 segments, 11/11 rhythms, content intact — PASS" on a wheel that was a twelve-colour pie chart. Both printed numbers stayed true, which is why nothing caught it. The twelve segments are <path> elements and survive their labels being removed. And the rhythm assertion searched document.body.innerText, where the SAME twelve rhythm words are rendered TWICE in every packet — once per wheel segment, once as a prayer-point heading. So "every rhythm word appears in the document" was satisfied by the points page alone and said nothing about the wheel. That is the self-consistency-vs-completeness family again, with a new twist: when two surfaces share one vocabulary, a document-wide string search cannot attribute a hit to the surface being asserted. The fix is scoping the query to the surface, not a better word list. wheelLabels counts the non-numeric <text> runs inside the .wheel-title sheet's own svg, excluding the twelve "5" pips and the "5 minutes x 12" footer. It counts ELEMENTS, NOT DISTINCT WORDS, deliberately: `praise` and `praise_end` collapse to one word in English — which is exactly why the existing line reads 11/11 and not 12/12 — and that collision is locale-dependent, so a distinct-word count would be a different number per language. The Driver's spec names the wheel as one of the packet's four sections, so 12 labels is a contract. Mutated packet reports 0/12 and fails; restored reports 12/12. i18n verified at the rendered layer with a partial locale block (4 of 12 rhythms translated): the wheel painted four XX- labels with the other eight falling back to English and zero blank — and the split pair is visible, segment 1 English PRAISE beside segment 12 XX-PRAISE_END, which is what per-key resolution looks like. All 11 locales regenerated with 95 photos each: check:packets-all 11/11, every one 12/12 labelled, including RTL ar and Devanagari hi. Wave 34/34. |
||
|
|
a177f98aff |
legend: the controls no longer read their own i18n key aloud
Some checks are pending
build-gate / build (push) Waiting to run
The legend's toggle button announced the literal string "aria.toggleLegend" to screen readers. Not in translated locales only — in English too. vue-i18n echoes the key when a lookup misses, and `toggleLegend` was absent from every catalog while its two siblings in the same file, backToLegend and closeDetailView, were present. Six call sites across LegendDesktop.vue and LegendMobile.vue, desktop and mobile alike. aria-label is the one string a sighted tester never sees. Every other untranslated string in this tree got caught because somebody looked at the screen; this one is only ever spoken aloud, so it survived every visual QA pass. A second instance turned up in the same sweep with the opposite shape: SemanticTreeLegend hardcoded 'Collapse legend' / 'Expand legend' into both title and aria-label with no t() at all — correct English, and untranslatable. One key missing FROM the catalog, one string never routed TO it. Adds toggleLegend, collapseLegend and expandLegend to all 11 aria.json catalogs and routes the hardcoded pair through t(). check:i18n-parity already guards them. New gate smoke:aria-labels asserts BOTH shapes, because either assertion alone is insufficient: "no raw keys" passes a hardcoded English label forever, and "labels differ across locales" passes a raw key, since a key is identical in every locale. It detects a raw key by SHAPE — dotted, camelCase, no spaces — which is locale-independent and works on the Arabic and Chinese pages where no English word list could. Mutation-tested twice. Removing the catalog key fails with the raw key named, on all four viewport/locale combinations. Hardcoding one label back to English initially PASSED: the differ count dropped 6 to 5 and nothing asserted on it — the same "a number printed but not asserted on is decoration" shape this tree keeps rediscovering — so the gate gained a LOCALIZED_FLOOR ratchet and now fails that too. Verified the fix generalizes beyond the profile it was found on: country-posters reports 182 of 190 accessible names localized, 0 raw keys. Rebuilds the six bundles that embed a legend; check:sri confirms every leaf matches. Wave 34/34. |
||
|
|
fb7ed4566d |
Browser gates now prove the server is serving THIS checkout
The gospel-resources gate was found adopting whatever answered on the default port, which on this machine was an unrelated process from a different worktree. It reported on a build that was not the one on disk: first a PASS, then a mount timeout that read exactly like a regression in code nobody had touched. "Answers 200" and "is serving this checkout" are different questions, and only the second one is the gate's job. That fix was local to one gate. The same default sits under the rest of them, so it is now one shared helper, internal/_serves-this-tree.mjs, fingerprinting public/doxa-maps/manifest.json — which generate-tree rewrites on every build and which names every bundle and profile in the tree. Different bytes means a different build: another worktree, or a server someone forgot to restart. Both produce the same lie. Wired into verify-browser-all, which hands ten gates their base URL and would have passed the wrong tree to all of them, and into check-a11y-button-names. Both now refuse with a NOT MEASURED exit rather than producing a confident wrong answer. The gospel gate keeps its behaviour of starting its own server instead. Verified in both directions: against the foreign 4173 each prints "serves a DIFFERENT build than public/" and exits 2; against this tree the aggregator proceeds to its ten gates and the gospel gate passes. ONE REPORTED NUMBER WAS WRONG BECAUSE OF THIS. The a11y result recorded earlier in this flight, "810 buttons, 810 with an accessible name", was measured against the foreign tree. Re-run against this checkout it is 825 buttons, 825 with an accessible name, still passing across 21 leaf pages. The conclusion held; the evidence had not been about this build. Audited the rest of this flight's browser evidence rather than assuming: the 41 probe scripts in _qa-cycle all target port 5399, which is served by serve-strict --directory <this worktree>/public, and the manifest served there is byte-identical to the local one. Those results stand. |
||
|
|
6af75895c7 |
legend: the info popovers explain the map in the reader's own language
Some checks are pending
build-gate / build (push) Waiting to run
The Driver asked for the per-tab info buttons to stay — "the tab itself gives
you a little information button that will explain what a UPG-100 list is."
They existed, and they were English-only in every locale.
SemanticTreeLegend rendered `tab.info` RAW in all three places it appears (the
button's title=, and the popover body) while the tab LABEL beside it went
through the catalog. So a Spanish reader saw a translated chip next to an
English explanation of what that chip means — and the explanation is the part a
reader who does not read English actually needs. The three UPG-100 tab labels
were absent from tabs.json entirely, so they fell back to hardcoded English too.
Measured on the served leaf before changing anything: ?lang=es changed 2 of 76
rendered strings ("POPULATION" -> "POBLACIÓN") and left all three tabs and all
three info popovers in English. That positive control is what proves the locale
switch WORKS and the strings were simply never catalogued — without it this
reads identically to a broken ?lang=.
tabInfo() mirrors tabLabel exactly: `legend.tabInfo.<i18nKey|id>`, te() guard,
falling back to the hardcoded English so a tab with no catalog entry renders
unchanged rather than blank. The v-if stays keyed on the raw `tab.info`:
whether the affordance EXISTS is a property of the tab definition, while its
TEXT comes from the catalog.
Catalogued in all 11 locales, labels and info. check:i18n-parity already guards
them — deleting one zh key fails the build by name.
New gate smoke:legend-info renders the REAL served page and clicks each info
button. check:tabs-localized could not see any of this: it tests 7 hardcoded
research-map tabs and REIMPLEMENTS tabLabel inside page.evaluate, so it proves
the mechanism works rather than that a given profile is localized — and it never
looked at `info` at all. Mutation-tested: reverting the popover to the raw prop
fails with "the catalog is present and unreached".
Partial-translation contract verified by deleting 2 of 3 fr info keys and 1 of 3
fr labels: translated keys render French, untranslated fall back to English,
nothing blank, and labels and info resolve INDEPENDENTLY.
Rebuilds the six bundles that embed the legend so the tree stays internally
consistent; check:sri confirms all 55 script references match. Full wave 34/34.
|
||
|
|
77ac7e3436 |
The two largest gaps on this map were Chinese varieties of a resourced language
Read top-down, the gap list sent you to "Chinese, Jinyu - 60,310,000 people - nothing found", then "Chinese, Gan - 41,280,000". Both statements are true about those ISO codes and both are misleading as priorities: ISO 639-3 files them under the macrolanguage zho beside cmn, Mandarin, which has Scripture, film, audio and training here. Measured across the whole map: 84 pins covering 140M people sit at "nothing found" while a sibling variety of their own ISO macrolanguage carries resources. Those groups now carry a related-variety note, on the pin sheet and inline in the ranked list, because the list is the thing that gets read top-down. 63 codes have one. WHAT IT DOES NOT SAY. It is not a claim of mutual intelligibility, and the wording refuses to imply one - spoken Jinyu and spoken Mandarin are not mutually intelligible; what they share is a written standard. Only ISO's own relation is asserted, the reader is told exactly that, and it changes NO tier and closes NO gap. Tiers are byte-identical at 2901/4767/4711. The chips still read "none found", because that is still true. The macrolanguage table is fetched from SIL rather than transcribed, and written through the snapshot guard like every other catalogue. Also corrected: the gap list header still said "Audio has never been searched for any of them". That stopped being true when Faith Comes By Hearing was added, and a stale caveat misleads exactly as much as a stale number. It now says audio HAS been searched and these groups have none there either, with GRN still named as the one pool nobody can look in. And a gate defect that produced three runs of false signal. verify-gospel-map adopted any server answering 200 on the default port. Port 4173 turned out to be held by an unrelated python3 process from another worktree, so the gate reported on a build that was not this one - first a PASS, then a mount timeout that looked exactly like a regression in code that had nothing to do with it. Answering 200 is not the same question as serving this tree. It now fingerprints the served bundle against the bytes in public/ and refuses to adopt a server whose build differs, starting its own instead. Proven: it now prints "serves a DIFFERENT build than public/ - not adopting it", starts its own, and passes. Screenshot read: the ranked list shows "related: Chinese, Mandarin has resources" under Jinyu, Gan and Min Bei, and correctly nothing under Deaf Chinese or Deaf Pakistani, which have no macrolanguage sibling. |
||
|
|
f129294609 |
Clause L: a verification record that stops being re-run now fails the gate
Eleven clauses checked that the surfaces AGREE with each other. None checked that any
of it was still true. "Link checked" is a claim about the present tense, and a table
that quietly stops being re-run keeps making that claim on evidence that could be a
year old while every consistency clause still passes. A dead link is the failure a
field worker actually hits, so this is not bookkeeping.
Clause L fails when a shipped URL carries no check date at all, or when the oldest
shipped link was last checked more than 45 days ago, and the message names the exact
command to fix it. Re-running is cheap and resumable now, so the failure is an
instruction rather than an obstacle.
Proven against real data, not only the poison harness: deleting the check date from
one shipped URL is caught ("1 shipped URL(s) carry no check date"), and backdating one
by 120 days is caught ("last checked 120 days ago, past the 45-day limit"). Both
restore clean. Poison self-test now 12/12.
Also checked and deliberately NOT changed: the Arclight harvest keeps a language only
when counts.featureFilm >= 1, and it was worth asking whether that discards coverage.
Of 2,330 catalogue languages, 2,309 already have a feature film. Only 15 carry just a
shortFilm or a series, and exactly one of those - nmg Kwasio, 2 pins, 60k people - is
spoken by a group here with nothing else. Widening the filter would mean typing a
short film as "Film" beside the JESUS Film for a single language. The filter stays.
The registered description still claimed "6/6 clauses must fail"; corrected to 12.
|
||
|
|
e8ad1d56e6 |
packet: a partial translation renders 12 prayer points, not 3
Some checks are pending
build-gate / build (push) Waiting to run
packet-render.js resolved the prayer points with `L.points || EN.points`. That is a whole-block fallback when the key is ABSENT, and TRUNCATING when a translator has done some of the 12 — a 3-element array is truthy. Measured at the rendered layer with a deliberately partial locale block: a page headed "12 Prayer Points" containing THREE, points 4..12 silently gone, including the Mark 5:1-20 passage point 4 prints in full. Nothing blank, no error raised. Invisible in all 11 shipping locales because every one of them is complete. It would have appeared on the next language added — and "a new language is a NEW BLOCK, never a code change" only holds if a PARTIAL block degrades per key, since half-finished is the normal state of a language in progress. Now merges per point and per field against English: English fixes the LENGTH and supplies every missing field, so a point translated for `text` but not `sub` keeps the English `sub` — and keeps its NUMBER, which the merge supplies. The sheet-sum invariant could not see this: a 3-point page is still exactly one sheet, so every number check-packet-render printed stayed correct while nine points were missing. It now measures and asserts the count. Mutation-tested — reverting the fix prints "prayer points : 2 / 12" and fails. All 11 locales regenerated with photos and swept: check:packets-all 11/11, each 12/12 points, including RTL ar and Devanagari hi. Renderer, generator and gate synced to doxa-maps-plugin-studio, which also lacked the .points-title and .wheel-title test hooks the new assertion needs. Also wires check:capability-exemplars into gates:post (33 -> 34 steps). It reads public/doxa-maps/manifest.json, a build artifact, so ordering mattered: `tree` writes the manifest and is &&-chained immediately before gates:post, and the gate SKIPs rather than fails when the manifest is absent. Full wave: 34/34. The quoted gates:post line in the README is re-synced in the same commit, because check:quoted-scripts goes red on the drift this commit creates. |
||
|
|
0e0b142719 |
A half-finished catalogue fetch can no longer overwrite a good one
The FCBH fetcher read per-fileset video scope for 1,666 languages and that file was
committed. A later run, while live.bible.is was rate-limiting, produced 1,103 and
overwrote the good file with the degraded one. Nothing failed, nothing warned, and the
artefact silently lost a third of its content. It was caught only because git status
happened to be read, and the fix was a git checkout.
That is the third time this shape has appeared here:
- a VERIFY_LIMIT smoke run replaced the full verification table with 30 rows;
- dropping a dead link removed it from the corpus, so the next run forgot it was
dead and the build put it straight back;
- this one.
Every snapshot in this bundle is built from a network read, and a network read has a bad
day. The dangerous case is never the run that throws - that leaves the old file alone -
it is the run that half-succeeds and writes anyway.
scripts/_snapshot-guard.mjs now stands in front of every catalogue write. A snapshot may
grow freely and may shrink a little, but losing more than a tenth of the previous run is
treated as a failed fetch: the existing file is left untouched and the script exits 1.
It is deliberately not an equality check, because catalogues do genuinely lose entries
when a publisher retires something, and a gate nobody can satisfy gets switched off.
SNAPSHOT_FORCE=1 is the one loud way past it.
Proven by replaying the exact failure: feeding it the 1,103-entry version refuses the
write, names the real numbers (563 fewer, 13.8%, below the 90% floor) and leaves the
file byte-identical to the good one. SNAPSHOT_FORCE=1 writes it. An equal write passes.
All six fetchers are wired: ebible, arclight, arclight-share-urls, door43, zume, fcbh.
Re-running zume, door43 and ebible for real passes the guard at 52, 208 and 1,237 codes
and produces byte-identical data, so the guard perturbs nothing.
The one exception is the checkpoint write in the resumable share-urls fetcher, which
starts from the existing file as a cache and can only grow. It must now declare itself
with a CHECKPOINT-WRITE-OK marker, so the exemption is a statement rather than an
oversight.
New gate check:snapshot-guard fails if any fetch-*.mjs writes OUT directly without that
marker, or never calls writeSnapshot at all - a guard that is easy to bypass is a guard
that will be bypassed. It refuses to run against an empty file list rather than passing
vacuously, and --poison injects a raw write and requires the check to catch it. Chained
into build:unlocked and its doc copy kept in sync.
|
||
|
|
510f3d30d4 |
The backlog-status enum was too narrow, not the nine cards wrong
Some checks are pending
build-gate / build (push) Waiting to run
This gate keeps the AGGREGATE honest — a prose status makes every open-count wrong
while each card still READS correctly, so the error hides in the total. That matters
to this loop directly: every tick starts by reading status counts to pick a card.
MEASURED across 633 cards: EIGHT distinct status values are in use and EVERY ONE IS
A SINGLE WORD. No prose remains anywhere in the field — which is exactly what the
original 130-card migration achieved. The nine cards this gate was failing carried
legitimate TERMINAL states the enum simply predated: superseded (5), deferred (2),
decided, verified-already-true.
★ REWRITING THEM TO "done" WOULD HAVE DESTROYED REAL DISTINCTIONS to satisfy a list.
A SUPERSEDED card was replaced by another; a DEFERRED one was consciously postponed;
neither was finished. Losing that information to make a checker green is the
opposite of what this gate is for.
OPEN vs CLOSED is the property that matters, and all four additions are CLOSED:
open = todo + doing + in-progress (66), closed = done + these four (507),
blocked (60).
MUTATION-TESTED BOTH WAYS, so widening did not weaken it: a prose status
("done — reconciled against main, see notes") still FAILS by name — the original
defect — AND an unagreed one-word status ("wontfix") ALSO fails, so the set stays
CLOSED rather than becoming "any single word".
Wired into gates:post (32 -> 33) after confirming it reads only backlog JSON. The
doc that quotes gates:post is re-transcribed in the same commit.
Build: 7/7 pre, 23/23 bundles, 33/33 post, exit 0.
|
||
|
|
c4fbc469ac |
Bible video from FCBH, with the real book scope on every link
The previous change left video_date out because the column does not say what the video
IS. Establishing it took measurement, and the first samples were misleading. Three
video-only filesets all rendered ...S2DV pages opening on "Intro & Garden of Eden" with
Drama/Non-Drama controls, which suggested one Old Testament series. Reading
GET /api/bibles/<bible_id>, which returns the exact books array in one call, for all
1,669 video languages says otherwise:
209 27 parts, Matthew to Revelation (the whole New Testament)
116 27 parts, San Mateo to Apocalipsis (same, Spanish editions)
110 12 parts, Intro & Garden of Eden ... (Covenant, Old Testament)
27 Mark 26 Acts 23 Luke
1,006 distinct scopes in all
Generalising from three samples would have described the bulk of it exactly backwards.
Every link therefore ships its own scope rather than a shared description, and the 3
languages whose book list could not be read were dropped: an undescribed video is not
shipped.
It is typed "Bible video", never "Film". Film on this map means the JESUS Film, and
bible.is itself treats the two as different - its pages call a separate Arclight
endpoint, /api/arclight/jesus-film/languages?iso=..., to find it. So Bible video lifts a
group off "nothing found", which is true, and contributes nothing to the Scripture+film
tier. 1,666 languages carry it: 287 as their own link reaching 4,375 pins and 5.76bn
people, and 1,379 merged into the Scripture link whose media list now reads "audio,
text, video" instead of advertising half of what the page holds.
Verification for bible.is switched from a body-size heuristic to their own API. The page
answers 200 for any id, so the first version compared body sizes against a shell
measured at run time. That was a heuristic on a number the site controls, and it stopped
working when their WAF began answering HEAD with 403 and a 520-byte body for real and
bogus alike. The run-time calibration caught exactly that and ABORTED rather than mark
thousands of links verified - which is the whole reason it was measured and not
hard-coded. The API needs no heuristic: /api/bibles/OKEBSN is 200, /api/bibles/ZZZZZZ is
404. It is a tenth of the weight, it describes the same fileset the page renders, and
GET is used because HEAD is what the WAF blocks.
Also fixed: a fetch with no timeout hung the fetcher for thirteen minutes with no
output. A stall looks exactly like slow work, so every fetch in it now carries a
timeout, and the scope lookups run with bounded concurrency and 403 backoff.
6,404 links screened, 6,557 verified, 74 broken and dropped, 5 uncheckable. Screenshot
read: German shows "Bible video - 12 parts, Intro & Garden of Eden to After the Forty
Years / End Credits" as its own chip, distinct from "Film - link checked".
|
||
|
|
28ce04f6ac |
check:done-evidence knew one card schema and failed the best-evidenced cards on the board
A done card with no record is the reporting failure this loop exists to avoid — so a gate that mis-identifies WHICH cards lack a record is worse than none: it teaches the next agent to pad the field the checker reads. ★ THE THREE FLAGGED CARDS WERE AMONG THE BEST-EVIDENCED THERE ARE. Each carried one_line, why, and a `verify` block naming the probe, its assertion count and its negative tests — "74 files match byte for byte, all three drift classes exit 1", "9 assertions, all pass", "two new assertions, both negative-tested". The gate looked only for `bluf` or `fields`. MEASURED across 443 done cards: 40 carry `verify`, 41 carry `one_line`, and ALL THREE flagged cards had one or both. `verify` is if anything the STRONGER signal — it names how the claim was checked, which is what evidence means here. Fixing the CARDS by adding a redundant bluf would have been gaming the check. Fixing the GATE to recognise the evidence already there is the honest repair. ★ THE BASELINE IS NOW EMPTY. All ten exemptions reported STALE — every one had since gained evidence — so the list exempted nothing while implying ten cards still owed a record. An EMPTY set is not an unset one: every done card must now carry evidence with NO exemptions, and the gate fails outright for a single card that does not. Keeping ten satisfied ids would mean the same today and quietly permit a regression the moment someone reused an entry. Same reasoning as emptying the i18n DEBT map. MUTATION-TESTED both directions: a done card with NOTHING in any of the four fields FAILS by name; the same card with ONLY a `verify` entry PASSES. Wired into gates:post (30 -> 31) after confirming it reads only backlog JSON, which the build does not mutate. The doc quotes gates:post, so it is re-transcribed in the same commit — the self-referential update noted last tick, handled in one pass this time. Build: 7/7 pre, 23/23 bundles, 31/31 post, exit 0. |
||
|
|
659ca6e244 |
FCBH text as well as audio, and 207 text-only filesets stop claiming to be audio
The FCBH integration used only audio_date. Their rows also carry text_date, and it is unambiguous - they publish Bible text on bible.is. 698 codes carry text where this map had no text Scripture at all, reaching 2,176 pins and 1.38bn people. Codes go 2,192 -> 2,400 and the gap falls 2,927 -> 2,908. ONE LINK PER LANGUAGE, NOT ONE PER MEDIUM. A bible.is fileset is a single page carrying whatever media it has, so two chips pointing at the same URL would be noise. The link now states what it carries: 1,465 codes audio+text, 677 audio only, 207 text only. That last number was a live falsehood. Because the builder never copied the media field across, all 2,349 bible.is rows were typed "Audio Scripture" - including 207 filesets with no recording at all. A group with text-only Scripture was being sent to look for a recording that does not exist. Both the chip and the table type now follow the media. Ranking is by media count, then AUDIO PRESENT, then recency. The middle term is not decoration: without it a newer text-only fileset outranks an audio-only one on date alone and the language silently loses its recording, which is the medium an oral-preference group actually needs. Measured while it was missing: 3 codes lost audio. video_date is deliberately left out. The column does not say what the video IS - a Luke film, a sign-language edition, something else - and counting it as "film" beside the JESUS Film would assert something nobody here has checked. 400 codes with no film today carry one, so it is worth establishing later, but not by guessing. Clause I now accepts BOTH Audio Scripture and Scripture on bible.is, because a fileset really can be either, while keeping the set closed and keeping Audio Scripture wrong on every other host. Proven by poisoning both directions: Film on bible.is and Audio Scripture on ebible.org are each caught. Poison self-test 11/11. Also verified, not assumed: the brief's requirement that a pin or country click filters the table. iso=deu shows 4 of 5,889, country=IND shows 502 of 5,889. It works; an earlier reading of mine that suggested otherwise was measuring the wrong thing. All 6,117 links screened and verified. Screenshot read: Ninigo (ssg) shows "Scripture, text - 2012 Wycliffe Bible Translators, Inc." where it would have claimed audio. |
||
|
|
482e6d4ca0 |
Wire the seven gates that could run, and make wave membership visible
Some checks are pending
build-gate / build (push) Waiting to run
MEASURED: 70 of 100 check/verify scripts were in package.json but in NEITHER gates:pre nor gates:post. I had fixed this same "present but unwired" failure three times for individual gates without ever asking how many others were in the state. ★ THE GATE THAT EXISTS TO CATCH THIS WAS ITSELF UNWIRED. check:no-orphan-gates asserts every gate-shaped file is REACHABLE — named in package.json or imported — and it passed, 87/87. But reachable and WIRED are different properties, and conflating them is exactly how 70 scripts sat outside every build. A script entry means a human CAN run it; wave membership means it RUNS. Tested each unwired gate standalone: 7 pass with no precondition and were simply never wired (now in gates:post, 22 -> 29 steps, +1.5s on a ~150s build); 6 FAIL on pre-existing documentation drift nobody sees — check:verification-tags alone lists 61+ untagged cards — which is its own project, and wiring a red gate only gets it disabled; the rest legitimately need a served tree, a device, a token or an argument. ★ ONE I WIRED WRONGLY, AND THE MISTAKE IS THE USEFUL PART. check:settled asserts the tree is QUIET — no builder running, no recent churn — so it fails inside a build BY DESIGN. It passed twice standalone, I wired it on that evidence, and the next authoritative build came back 29/30 with check:settled the only red. Running a gate twice while IDLE proves repeatability, not compatibility with running mid-build. The question for wave membership is not "does it pass" but "is its assertion compatible with a build in flight". Unwired, and the reasoning is recorded in check-no-orphan-gates so nobody re-adds it on the same evidence. check:no-orphan-gates now reports wave membership as a second, distinct question from reachability — "37/93 run in gates:pre or gates:post" — naming the first eight unwired so the debt is visible rather than silent. It REPORTS rather than fails, because most of the remaining 56 are correctly hand-run. Mutation-tested: removing check:zone-census from gates:post moves the count 37 -> 36. Build: 7/7 pre, 23/23 bundles, 29/29 post, exit 0. |
||
|
|
a11e19a6b0 |
Audio Scripture in 2,121 languages: every pin used to say "not searched"
Every pin on this map has read "Audio - not searched (GRN)" for its whole life, because
Global Recordings Network sits behind Bunny Shield and answers 403 to everything, its
own robots.txt included. Re-probed this tick: still 403. But GRN was never the only
audio pool.
Faith Comes By Hearing needed no key. Their 4.dbt.io API wants one and answers 422,
which is where the earlier note stopped. Their own recordings-database page does not
use that API - it is client-rendered and pulls a STATIC public file from their CDN,
bible_recordings.json, 3,602 rows of { iso, lang_name, bible_name, bible_id, country,
video_date, audio_date, text_date }, already keyed by ISO 639-3. 2,192 codes carry a
dated audio recording; 2,121 survive verification.
Audio lifts a group off tier 0, unlike Zume, because a people group with an audio Bible
in their own language does not have "nothing" - for an oral-preference group it may be
the most useful thing on the list. It does not create tier 2, which stays reserved for
text Scripture together with film. Measured: 533 pins and 59.5M people leave the gap,
which falls 3,460 -> 2,927. Coverage 72.0% -> 76.4%. 8,094 pins now carry audio.
A 200 FROM live.bible.is PROVES NOTHING and this almost shipped wrong. It answers 200
for a fileset that does not exist, returning a fixed-size not-found shell, so the
discriminator is BODY SIZE and HEAD gives content-length for free. The shell size is
measured at run time, never hard-coded: it was 7,961 bytes on one path and 7,860 on
another within the same hour. Both fetcher and verifier abort if a bogus id ever
returns the same size as a known-good one. It caught 73 dead filesets in FCBH's own
data, all dropped.
The first full pass collected 466 HTTP 403s from bible.is - rate limiting, not dead
links; the same URL answered 200 when retried alone. Requests are now paced per host
and 403/429 retried with backoff, which cut those to 48, and a VERIFY_RETRY_UNKNOWN
pass resolved all 48. Final: 5,882 verified, 73 broken, 5 uncheckable.
Three bugs found and fixed on the way:
- The dead-link drop ran BEFORE the audio merge, so 71 dead filesets attached after the
cleanup and shipped anyway while the pass reported "dropped 0". A cleanup that runs
before the thing it cleans is worse than none, because its count measures an empty
room. Moved after every source attaches.
- A VERIFY_LIMIT smoke run overwrote the real table, destroying the broken-link history
the previous change added. A limit run is now a smoke test that writes nothing.
- The browser gate screened 3,768 of 5,889 links because its harvest never learned the
new field.
Added VERIFY_MISSING=1 so adding one resource costs one request, not a quarter-hour
pass over 5,960 URLs.
Charts, list and map were corrected together: all three still asserted audio had never
been searched, and charts deliberately drew it as a hatched "not measured" row. That
bar is now a real count, decided by hasField() from the data itself. The checked-pools
caveat on every group now names fcbh.
Screenshot read: Uab Meto (aoz) - no Scripture, no film, no stories, not in Zume - now
shows a verified audio link and the caveat lists fcbh as checked.
|
||
|
|
18db274d72 |
The packet gate asserts RTL LAYOUT, not just the dir attribute
Some checks are pending
build-gate / build (push) Waiting to run
Arabic is one of the eleven locales, and RTL is the one layout property that can be
declared correctly and still not take effect. The gate asserted
document.documentElement.dir === 'rtl' — necessary, and not sufficient. Any inner
rule can override it while every other assertion here stays green, and an Arabic
packet with readable glyphs in LTR order reads as a styling quirk rather than the
bug it is.
It now samples up to 25 leaf text runs inside .sheet and measures the painted
extent against the element box. An RTL packet must pin its runs to the RIGHT edge,
an LTR one to the LEFT. The signal is unambiguous: en 0 right / 25 left,
ar 25 right / 0 left. Threshold is 80% of sampled runs, so one legitimately mixed
line cannot trip it.
★ MEASURE THE INLINE TEXT RUN, NOT THE BLOCK. My first probe compared each
element's box to its parent's and returned IDENTICAL numbers for Arabic and
English — it was measuring container geometry, which does not move when direction
flips, and would have "passed" an RTL packet laid out LTR. A Range over the text
node gives the painted extent, which does move. Centred text is skipped because it
proves nothing either way, and so is text that fills its box — no room to tell.
MUTATION-TESTED with the exact failure this guards: injected .sheet{direction:ltr}
so dir="rtl" REMAINS DECLARED on <html> while the layout reverts. FAIL, both
symptoms named — "only 0/25 sampled runs do (right 0, left 25) — dir=\"rtl\" is
declared and not taking effect", and "25/25 sampled elements compute
direction:ltr". All 11 locale packets pass with it live.
Verified a NEW RTL LANGUAGE end to end: Urdu added as a DATA BLOCK ONLY — 2 of 21
keys, no code change — renders dir=rtl with 25/25 runs painted right, 95 photos,
translated keys in Urdu, untranslated falling back to English, zero blank headings.
The packet's i18n contract holding for a language that did not exist when the tick
began.
|
||
|
|
96d7902a95 |
Sweep every browser gate for English-text matchers; fix the fifth and last
Some checks are pending
build-gate / build (push) Waiting to run
The same defect had surfaced in FOUR gates across previous ticks — a gate keyed on
English prose silently excludes every translated surface, or goes red on a
correctly translated one. Finding a fifth by accident would mean the pattern was
still being discovered rather than eliminated, so this sweeps all 25 gates that
drive a browser instead of waiting for the next one.
9 flagged, 8 benign: CSS class names (.vtl-fab, .stl-panel) and CSS property VALUES
(/paint|layout|strict|content/) are locale-independent by definition.
ONE REAL HIT — check-poster-prints.mjs:131, `/Gadarenes/.test(...)`. That is the
World English Bible's proper noun, so the assertion and the bug were locked
together: it passes today ONLY because point4_passage is still English in all ten
non-English blocks. The day a licensed translation lands it would have gone red on
a CORRECT packet. Now asserts the .p4-passage block by length, the same structural
fix already applied in check-packet-render and check-packet-browser.
Proved on the case the old matcher got wrong: renaming Gadarenes -> gadarenos, as
any translation would, now PASSES where /Gadarenes/ would have failed.
MUTATION-TESTED both directions — a passage truncated to 180 chars FAILS ("looks
truncated: 259 chars"), and the .p4-passage block renamed in the MARKUP FAILS
("no .p4-passage block").
★ ONE MUTATION SILENTLY DID NOT LAND, and it is worth recording. My first attempt
at the second mutation used a count=1 replace, which hit the explanatory COMMENT
rather than the markup — "p4-passage" appears twice in packet-render.js. The gate
"passed" and I nearly recorded a hole in the assertion. The assertion was sound;
the mutation was not. The sweep made the same class of error in the other
direction, flagging check-packet-render for a "Gadarenes" that lives only in a
comment explaining why it was removed.
Five instances found and fixed across five ticks; none remain in live code. The
rule for any new gate: if it matches on user-visible text, resolve the expected
string from the catalog for the page language, or assert on STRUCTURE — a class
hook, a length — instead.
|
||
|
|
3519ca3934 |
check:packet-browser asserts its headings, and stops reading them in English
This gate printed nine metrics, folded six into `const bad = a || b || ...` — so a
failure said "FAIL" without naming which — and left three unchecked: page size and
the two headings, which are the very strings its own header claims it covers ("the
gate reports which headings came back translated"). Reports, not asserts.
★ IT LOOKED LIKE IT HAD FOUND A PRODUCT BUG, AND HAD NOT. Under ?lang=es it printed
an EMPTY wheel heading; under hi it blanked BOTH — while the catalog holds "Rueda de
Oración" and "प्रार्थना चक्र". That reads exactly like a broken i18n path, and it
PASSED anyway, because nothing asserted the values.
The cause was the harness. wheelTitle was innerText.match(/^.*Wheel.*$/m) and
pointsTitle /Prayer Points|Puntos/ — neither can match Hindi, and /Wheel/ cannot
match Spanish. The product was correct in every locale all along: verified after the
fix in en/es/hi/ar/zh, RTL and Devanagari included. Had I reported that as a defect
I would have been wrong.
mark5 carried the same fault: innerText.includes('Gadarenes'), an English proper
noun that goes RED the moment point4_passage is translated. Now asserted by
.p4-passage length — the same fix already applied in check-packet-render.
Headings are now compared to the CATALOG entry for the packet's language, with
per-key fallback to English exactly as the renderer resolves it. packet-render.js
gains class="points-title"/"wheel-title" as TEST HOOKS: both were inline-styled with
no selector, which is what forced the English greps in the first place.
MUTATION-TESTED: a renderer forced to ignore the language FAILS with both headings
named and the expected values shown; a renderer emitting an empty heading FAILS
("a blank heading is worse than an English one").
★ ONE MUTATION CORRECTLY PASSED, and it marks a real limit: reverting es
wheel_title to English IN THE CATALOG passes, because rendered and expected then
legitimately agree. This gate cannot tell a translated catalog from an untranslated
one — that is check:packet-strings-parity's job, and it does assert it. Two gates,
two questions.
Partial translation re-verified through the BROWSER path: a 1-of-21-key Swahili
block renders "Mambo 12 ya Maombi" with the wheel heading falling back to "Prayer
Wheel" — translated where present, English where not, neither blank.
That makes four gates with one root cause (tabs-localized, packet-render,
deck-click, and this one twice). Any new gate matching on user-visible text should
resolve it from the catalog.
|
||
|
|
53ae954a7f |
The deck gate asserts its portraits, and can finally run in every locale
Some checks are pending
build-gate / build (push) Waiting to run
Two gaps on the same surface.
1. "116 portraits" was collected, printed, and NEVER asserted — only the QR codes
were checked against the card count, so a deck of 116 cards with ZERO portraits
passed. A prayer card without a face is a data tile, which is the whole difference
between the deck and a spreadsheet. Not hypothetical: card-render.js records that
the deck once shipped with no photos at all, because image_url was never requested
in fetchListRecords.
THE COUNT CANNOT BE PINNED — roughly half the groups have no portrait and the CDN
drops some — so two invariants instead: portraits + typographic === cards (a card
that is NEITHER means a third shape nobody checks), and a deck drawn from records
carrying image_url must have SOME, with DECK_EXPECT_PORTRAITS=0 to declare a
deliberately photo-less deck out loud.
2. The gate matched /card deck/i, so it could ONLY run in English. Pointed at
?lang=es it reported "no Card Deck button" and listed 28 buttons all reading
"Baraja de tarjetas" — the UI correctly translated, the GATE unable to see it. The
card deck is a core prayer artefact and had zero coverage in any of the ten
non-English locales, which is exactly where a rendering bug is most likely. The
label now resolves from the catalog for the page's own locale.
MUTATION-TESTED THROUGH THE REAL CAUSAL PATH, not synthetic DOM edits:
- image_url removed from the records provider (the historical regression):
FAIL, "116 cards and NOT ONE portrait", naming the cause.
- the no-photo branch renamed so cards are NEITHER shape:
FAIL, "116 cards but 0 portrait + 0 typographic = 0".
- DECK_EXPECT_PORTRAITS=0 on that same deck: PASS, sum invariant still holding.
- es cardDeck changed in the CATALOG without rebuilding: FAIL — so this now also
catches catalog/bundle drift instead of passing on a stale bundle.
VERIFIED IN FOUR LOCALES, each matching its OWN label — en "Card Deck",
es "Baraja de tarjetas", ar "مجموعة البطاقات", hi "कार्ड सेट" — 116 cards, 116
portraits, 116 QR codes, @page 2.5in x 3.5in, 0 page errors in every one.
Same two patterns have now recurred three times: a count printed without an
assertion, and a gate keyed on English text that silently excludes every
translated surface. Worth checking for on any new gate.
|
||
|
|
2bf4001d7e |
Door43 spelled 32 major languages with two letters, so 2.87bn people saw none of it
The Door43 fetcher kept only 3-letter codes and dropped 2-letter ones on the grounds that turning one into ISO 639-3 would be a guess. The caution was right and the conclusion was wrong: ISO 639-3 publishes the correspondence itself, in the Part1 column of iso-639-3.tab. The file is now fetched rather than transcribed, so it cannot go stale, and the run aborts if it comes back with fewer than 150 pairs. Measured cost of the old behaviour: 32 languages spoken by 2,576 pins - Bengali, Hindi, Spanish, Amharic, Hausa, English and more - carried Scripture and film but no Door43 resource at all, purely over a spelling. Door43 reach goes from 169 codes and 1,051 pins to 208 codes and 3,627 pins, 3.58bn people. Table rows 226 -> 391. ONE EXCLUSION, AND IT IS LOAD-BEARING: a tag with an '-x-' private-use subtag is skipped on this path. Door43 publishes el-x-koine, Koine Greek, whose base 'el' maps to 'ell' - Modern Greek, which 13 people groups on this map speak. Folding it in would have handed those 13 groups a Koine Greek text as though it were their own language. Region and script subtags (es-419, pt-br, sr-latn, ur-deva, zh-hant) are not private use and are the same language, so they still fold to the base. Verified: ell is absent from the index. Fixed a self-erasing bug in the previous change. Dropping a dead link removed it from the corpus, so the next verification run never saw it, so the next build put it back - measured, both links came straight back and REAL URL returned to 8,922. The verification table is now a RECORD, not a snapshot: it carries forward every URL a previous run proved broken and keeps re-testing them, which is also what lets a link the publisher later repairs come back on its own. Proven by dropping them and re-running: "re-testing 2 URL(s) a previous run recorded broken", still broken, build now stable across two full cycles. Clause K's 95% coverage floor is gone, because that floor is exactly how this hid: two missing URLs out of 3,768 is 99.9%. Every shipped URL must now appear in the record. Poison self-test 11/11. All 3,768 links re-verified: 3,761 live, 5 uncheckable, 2 broken and dropped. Also fixed the chip overflow this change caused - a Door43 entry can be "Aligned Bible (translationCore-Create-BCS) - Door43 - Portions of both Testaments", which ran off the edge of a 330px sheet unreadable. Long chips wrap now. Screenshot read: Hindi shows seven verified Door43 resources, all inside the panel, where it read "none found". |
||
|
|
8f7a2f875d |
Gate the default drop: with no poster and no source, nothing prints and the reader is told
check:poster-print-fallback proves the GOOD path — no .webp, .html present, browser
rasterises. On a tree with NEITHER it SKIPs. And neither is the DEFAULT, since
posterSources and bundlePosters both default false, so the behaviour a reader is
most likely to meet was the one behaviour nothing asserted.
BUILD-POSTERS-GENERATE-ON-DEMAND-DROP-52MB claims in prose that export and print
"degrade honestly, with the preview refusing to open rather than printing an error
page". That claim was true and untested. Measured on a real dropped folder: the
wall requests the .webp (404), then the .html fallback (404), then shows "poster
not found (404)" with a Close button. 0 print frames, 0 downloads, 0 page errors.
The failure this guards is NOT a 404. It is a print dialog that OPENS on a poster
that does not exist — a blank sheet, or a low-resolution PREVIEW thumbnail printed
as though it were the 3ft poster. Both look like the feature working.
MUTATION-TESTED, two plausible wrong fixes, each caught distinctly: falling back to
printing the preview ("1 print frame(s) opened with NO poster and NO source"), and
failing silently by clearing exportState ("the reader was told nothing"). The gate
also asserts BOTH paths were attempted, so a wall that gives up before trying to
rasterise fails even while showing the right message.
★ SERVE THE TREE ROOT, NOT public/ — and this is in the gate's header because I got
it wrong first. A leaf resolves assetsBase to '../../assets', which from
<tree>/<bundle>/<profile>/ is <tree>/assets. Serving public/ puts public/assets on
that path, a directory still holding 378 posters in a dev checkout, so the poster
loads with a 200 and the gate tests the opposite of what it claims. The same click
returned 200 under `--directory public` and 404 under `--directory public/doxa-maps`.
My first reading of this was "a print frame opened on a missing poster", which would
have been a serious product defect; it was the harness.
Also audited check-packet-render this tick: all thirteen printed metrics now have
assertions behind them. No decoration left on that surface.
|
||
|
|
101653e3fd |
Verify all 3,603 links: 2 were dead, and the map showed "unchecked" on the rest
The brief's rule is to verify a URL resolves before shipping it. The table that did that was written in August against a corpus of 426 links, while the map now ships 3,603 - and the UI matched exactly TWO of them. Two silent causes: its eBible keys were http:// while every shipped URL now passes through httpsify, and its 306 film keys were the old ?languageId= URLs that were later replaced wholesale by Arclight shareUrl deep links. Links that HAD been verified displayed as "unchecked", which a reader cannot tell apart from "nobody looked". The old note said the film page was unverifiable by fetch, and for the URL shape it was written against that was right: ?languageId=13562 and ?languageId=99999999 return byte-identical HTML, so a status check there is a false green. The shareUrl migration changed that. A path discriminates: /watch/jesus/german-standard -> 200, 944,516 bytes /watch/jesus/pashto-yusufzai -> 200, 810,741 bytes (distinct body) /watch/jesus/definitely-not-xyzzy -> 404 So 2,049 film links became checkable for the first time. The 5 remaining ?languageId= URLs stay "unknown" with that reason - never verified, never broken. Every host was probed with a known-good and a known-bad before being trusted, and all answer HEAD and discriminate: ebible.org, png.bible, alkitab.pw, jesusfilm.org, arc.gt, git.door43.org, zume.training. HEAD is deliberate - a GET over 2,049 film pages is ~1.6GB of someone else's bandwidth to learn a status code. Five negative and five positive controls run in band and the script aborts rather than write a table if either arm misbehaves. Result over 3,603 unique URLs in 365s: 3,596 verified, 5 unknown, and 2 genuinely dead - arc.gt/092vn (Yakpa, 2 pins) and /watch/jesus/4101 (Zhuang Liujiang, 1 pin), both re-confirmed 404 with a browser UA and a GET. The builder now drops any link verification proved dead BEFORE tiers are computed, so a code whose only film is a 404 has no film and its tier says so. The gap filter moves 3,457 -> 3,460, which is exactly those three pins. Also fixed: the verification fetch hung off openPin's country branch, so a tap landing before the country index had loaded left the whole session reading "unchecked". It is now fetched unconditionally. Lookup coverage goes from 2/3,605 to 3,603/3,603. New gate clause K fails below a 95% coverage floor or if any shipped URL is recorded broken; poison self-test now 11/11. Verified in a browser on the exact case that failed - one immediate pin tap - which now fetches the table and renders "Scripture - link checked" and "Film - link checked"; screenshot read to confirm both chips are green where they were grey. |
||
|
|
e04509a94d |
Pashto: 47.6M speakers were told Zume had nothing, while its Pashto site was live
Last change joined Zume by exact ISO 639-3 and left pus unjoined, which turned into a false statement on the map: 13 people groups covering 47.6M speakers read "Discipleship training - not in Zume's 54" while https://zume.training/pus was live and in Pashto the whole time. Absence of a variety label is not absence of a resource. Research for something that selects a variety found nothing that does. The publisher's launch article scopes Pashto as Afghanistan's other official language and a major language of northwestern Pakistan, especially Khyber Pakhtunkhwa and parts of Balochistan - territory spanning all three of pbu, pbt and pst rather than selecting one. The locale pages declare no language metadata at all: no hreflang alternates, no BCP-47 tag, and og:locale is the site-wide en_US on every locale including this one. So it ships as an APPROXIMATE match rather than an exact join or a silence: its own field, its own dashed chip that reads "Zume, in Pashto (variety not stated)", its own scope text in the table, never counted as an exact join and unable to move a tier. The gap filter still cuts 12,379 features to 3,457. It is a curated three-code list, not a runtime name match - an approximate join a reader cannot see and a gate cannot check is the one that eventually ships a wrong link. Why this is defensible here and would not be for film: Zume in Pashto is a written course in one literary standard, where the pbu/pbt/pst split is largely spoken variety. The JESUS Film is already in this index under separate per-variety dubs (pashto-yusufzai, pashto-southern) because a dub differs by pronunciation. The medium decides. New gate clause J fails if any code carries both a confirmed and an approximate training link, or if an approximate entry lacks a url, language or note - that is the corruption that would make the qualified claim render as the confirmed one. Poison self-test now 10/10. Both gates also harvest the new field, so its URLs are screened rather than invisible; the browser gate screens 3,605. Verified in a browser with three controls: deu renders gm-chip ok with a real href, kri renders gm-chip meh with no href, pbu renders gm-chip approx linking https://zume.training/pus and carrying "variety not stated". Screenshot read: the dashed chip is visibly distinct from both. |
||
|
|
2ed595c0f8 |
Zume Training: 52 languages of discipleship course, joined to 3,479 pins
The brief names zume.training every tick and an earlier pass recorded the list as unobtainable. That was wrong. robots.txt Disallows /api/ and the homepage switcher is client-rendered, both true, but robots.txt also DECLARES a sitemap, and the publisher maintains /articles/zume-languages for exactly this purpose. Neither is behind a Disallow. The real figure is 62 locales, not the ~40 estimated: 54 serve a landing page that answers 200. The two sources disagree in both directions - Dari and Pashto ship sites but are missing from the article's table, and eight table rows serve no page yet - so fetch-zume-index.mjs takes the union and then fetches every landing URL. A locale ships a link only if its own page answered. Naming and linking stay separate facts. Pashto is deliberately left unjoined: pus is a macrolanguage, the people-group data carries pbu, pbt and pst, and nothing the publisher gives selects one. Training is not Scripture and not film, so it sits in its own field and its own type and moves NO tier. Measured: the 52 joined languages reach 3,479 pins and 4.78bn people, and none of them are tier 0 - every Zume language already has Scripture or film. Folding training into the tiers would have cut the gap count without anyone having translated anything. The gap filter still cuts 12,379 features to 3,457. Both gates had a hole this exposed. The coverage gate harvested URLs from s, f and d only, so the 52 new URLs existed in no pin record it could see; and clause I checked Film, Scripture and Bible Stories by host but let any other type fall through unchecked. Both now cover training, and a poisoned row typed Scripture on zume.training is caught. The browser gate screened 3,550 links and silently ignored the new ones; it now screens all 3,602. Verified in a browser with a negative control: deu opens with a chip linking https://zume.training/de, kri opens with "not in Zume's 54" and no href, no page errors. |
||
|
|
20c1c0d325 |
Delete the 100-list wall's dead hidden-map export, and stop it lying to a gate
Some checks are pending
build-gate / build (push) Waiting to run
renderFor() had ZERO call sites and was the only caller of makeHiddenMap() and
loadPoster(); EXPORT_SPEC was never read. The whole chain dated from when this wall
rendered a poster by spinning up an off-screen map and compositing tiles. Export and
print now take the shipped .webp, or rasterise posters/<id>.html in the browser.
52 lines out, 17 of explanation in: 925 -> 888.
★ IT WAS NOT MERELY UNUSED — IT WAS CORRUPTING A GATE. isMapLeaf() in
generate-tree.mjs is a REGEX over FILE TEXT, so the engine handle inside the
unreachable function flagged this profile isMap=true. That flag made
verify-map-paint.mjs EXCLUDE the profile, with a written justification —
"constructs a real gl.Map ON DEMAND during poster export, off-screen at
left:-9999px" — where every clause was true of the SOURCE and false of the RUNNING
PRODUCT. A profile was excluded from a paint gate on the strength of code that never
ran, and the exclusion read as a measured product fact.
Deleting the chain flips the manifest to isMap=false, which is correct: the poster
wall is a wall of cards, not a map. The gate's comment now says it is absent because
it IS NOT A MAP, rather than because it is a map the gate cannot see.
⚠ The explanation had to be written carefully. Naming the engine handle whole in a
comment RE-TRIPS the same regex and reinstates the bug, so it is spelled in two
pieces on purpose with a note saying why — the same class as asserting Mark 5:1-20
via the English word "Gadarenes": describing a thing in the terms the detector scans
for makes the description itself the defect.
ALSO FIXED, found while verifying: check:poster-print-fallback probed
'../assets/posters/<id>.html', one level up. A leaf lives at
<tree>/<bundle>/<profile>/index.html and resolves assetsBase to '../../assets', two
levels up — so on a tree that DID carry the sources the probe 404'd and the gate
SKIPped with "posterSources:true was not set" when it had been. A precondition check
that is wrong about the precondition silently turns the whole gate off.
VERIFIED AT THE RENDERED LAYER after the deletion, not before: the print path still
rasterises with every .webp aborted ("generating the poster", clean terminal state),
and the card deck still prints 116 cards at 2.5in x 3.5in with 116 QR codes and 0
page errors.
|
||
|
|
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. |
||
|
|
e0f2d522bb |
All eleven locales complete — the last 162 keys of i18n debt, and the ratchet empties
Some checks are pending
build-gate / build (push) Waiting to run
regions.json for it/ro/zh, 54 keys each. check:i18n-parity now reports "0 key(s) of
known translation debt remain", closing the 429-key gap it found when it was written.
★ THE DEBT MAP IS EMPTY RATHER THAN FULL OF ZEROS, and that is the whole point of
this ratchet: a locale with NO entry must be complete — the gate fails it outright
for a single missing key — so an empty DEBT is the strongest state it can be in, not
an unset one. Writing { it: 0, ro: 0, zh: 0 } would mean the same today and quietly
permit a regression the moment someone "adjusted" a number. Mutation-tested both
ways: removing ONE key fails ("it is missing 1/176 key(s) and has no DEBT entry"),
deleting the whole file fails (54/176).
THE CARD'S ESTIMATE WAS WRONG AND IS CORRECTED. It recorded "54 keys of full
SENTENCES". Measured: 45 short labels (median 14.5 chars) + 9 longer strings, three
carrying %count%/%countries%/%region%/%country% interpolation tokens. The catalog is
keyed by its own English TEXT, not symbolic keys — which is exactly why the gap was
invisible: the fallback renders the English key itself, so key and value are
identical in English and nothing ever looks broken.
PLACEHOLDERS HELD BY A PROGRAMMATIC CHECK, not by care: every %token% in every
English value must appear, same set, in all three translations. 0 mismatches, 0
empty values. Mangling one breaks interpolation at runtime with nothing to catch it.
VERIFIED AT THE RENDERED LAYER on the regions-list leaf, en/it/ro/zh: labels
translated (Regions -> Regioni), the intro sentence translated, and the INTERPOLATED
count sentence correct — "315 people groups in 41 countries" -> "315 popoli in 41
paesi", both placeholders substituted with real numbers. Zero page errors. Each
locale asserted to DIFFER from English, so a silent fallback could not pass.
★ That sentence is the exact bug regions-list.vue:144 records: the Russian string was
"ALREADY in library/i18n/locales/ru/regions.json — copied, never wired", caught by
reading a screenshot rather than by any assertion. Now it is asserted.
zh is SIMPLIFIED, measured not assumed: the existing zh catalogs carry 33
Simplified-only characters against 4 Traditional. Vocabulary follows each locale's
own established terms — Popolo / Popor / 民族, Regione / Regiune / 区域 — and WAGF
stays an acronym, as fr and de already had it.
|
||
|
|
95b5586cc5 |
Assert the packet photos the spec names — "20 per page WITH IMAGES"
Some checks are pending
build-gate / build (push) Waiting to run
check-packet-render printed "95 cards (0 photos, 95 typographic)" and PASSED, in
every locale. The packet spec names the photos explicitly, so that was the last
unasserted number on this surface.
The cause was my own fixtures, not the product: I regenerated the packets with
NOPHOTOS=1 two ticks ago for speed. Regenerating properly gives 95 photos, 3.5MB,
0 broken. But a gate that passes a packet violating a core spec line is the defect
whoever made the artefact, and this one would not have noticed a real photo-fetch
failure either.
★ THE COUNT CANNOT BE PINNED, so this is two invariants rather than "=== 95".
Roughly half the groups genuinely have no photo, the CDN drops some — de came back
94/95 on this very run, which is exactly why a hard count would have been wrong —
and NOPHOTOS=1 is a SUPPORTED dev mode for fast layout iteration. So:
1. photos + typographic === cards. Every card is exactly one shape; a card that
is neither means the renderer emitted a third nobody is checking.
2. a photo-enabled packet must have SOME photos. Zero is acceptable only when the
run declares itself photo-less via PACKET_EXPECT_PHOTOS=0 — "deliberately
none" said out loud, rather than indistinguishable from "silently none".
MUTATION-TESTED BOTH. Removing a single .card img yields "95 cards but 94 photo +
0 typographic = 94". The photo-less packet FAILS by default and PASSES under the
opt-out. (My first attempt at the sum mutation silently did nothing — the card
blocks are 11,340 chars and I edited a 1,200-char window — so the assertion only
counts as tested because the second attempt targeted the real .card img.)
All 11 locales regenerated WITH photos and re-swept: 11/11 pass. i18n re-verified
end to end — a partial Swahili block, 3 of 23 keys, added as DATA ONLY with no code
change: translated keys render in Swahili, untranslated fall back per key to
English, zero blank slots.
|
||
|
|
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
|
||
|
|
ff8a318e51 |
Let the map gate start its own server, so it will actually run
Registering a gate is not enough to make it run. verify:browser-all is invoked NOWHERE in this repo; build:unlocked runs only `bun run verify`, and verify-all.mjs skips anything marked browser:true unless --full is passed. That is precisely how two gates rotted here before — "both had rotted unnoticed precisely because the build chain has never run them". A gate whose precondition is "remember to start a server first" has a precondition nobody remembers. So the precondition is gone. If nothing answers, the gate spawns serve-strict on an ephemeral port — ephemeral so two gates running at once cannot collide on 4173 — and shuts it down afterwards. `bun run verify:gospel-map` now works from a clean shell with no setup. An explicitly named BASE_URL or argv is honoured and never auto-served: a caller naming a host means it, and quietly pointing at a different tree would be worse than skipping. Verified — a dead explicit URL SKIPs and starts nothing. ★ THE FAILURE PATH LEAKED A PROCESS AND THE PASS PATH DID NOT. process.exit(1) inside the try terminates before the finally block's cleanup runs, so a failing run left its spawned server alive: measured 0 strays after a pass, 1 after a fail. A gate that leaks only when it fails leaks exactly when someone is re-running it to see what broke. The exit code is now set, not thrown, and the process exits after cleanup. Re-measured from a clean baseline: 0 strays after pass, after fail, and after restore. One scare worth recording: a stray server DID show up mid-test and it was the orphan from the pre-fix run, six minutes older than the code being tested. Checking its start time rather than its existence is what told them apart. |
||
|
|
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. |
||
|
|
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. |
||
|
|
02351725ba |
Gate the shape of last tick's bug, not just the instance
70 story sets were typed as Scripture for seven ticks because NOTHING compared a row's label to where it points. Clause H, added last tick, catches that exact mislabel. It would not have caught the same mistake made anywhere else. Clause I asserts the general shape: Film only on jesusfilm.org or arc.gt, Bible Stories only on git.door43.org and only under that name, Scripture never on a film host. Audited across all 3,550 rows first — 0 violations, plus 0 duplicate (language, url) pairs — so this ships as a guard rather than a fix. The 8 languages carrying more than one Scripture URL were checked and are correct: Cebuano really does have an eBible ULB alongside Door43's ULB and UDB, which is three translations, not three copies. THE PASS LINE NOW NAMES EVERY CLAUSE. It listed four of nine, which reads as a narrower check than it is, and the next reader calibrates their trust on that line rather than on the source. Nine clauses, nine names, and --poison still fails 9/9 on demand. The table also states its scope breakdown where a reader meets it — 855 New Testament, 254 Portions of both Testaments, 136 Full Bible, 14 Old Testament, 13 Portions — with the reason it is counted from each publication's own book list rather than its subject label. "How much Scripture exists" is the question under every other number in that table and it was previously visible only by sorting a column. Verified: gate passes, self-test 9/9, the table renders its new Scope column, and pins, scope and deep-link probes all pass. |
||
|
|
25405d571a |
Scope the Door43 Scripture, and stop 70 story sets being typed as Scripture
Door43 entries carry their own book list. Each ingredient is categorised bible-ot or bible-nt, so a Door43 Scripture entry can say how much Scripture it is, exactly as an eBible one does — and it is already in the response we fetch, so this costs no extra requests. 35 entries scoped: 15 New Testament, 13 Portions, 4 Full Bible, 3 Portions of both Testaments. THE SUBJECT LABEL IS NOT THE SCOPE, which is why it is counted from the data. An entry filed under "Bible" can be 27 NT books and nothing else. Calling that a Bible because of its folder name is the same error as calling Open Bible Stories Scripture. ★ AND THAT ERROR WAS ALREADY SHIPPING, 70 ROWS OF IT, BY MY OWN HAND. Tick 15 added owner disambiguation so two same-named entries could be told apart — "Open Bible Stories (OBS-TLF)". The type test was `d.s === 'Open Bible Stories'`, an exact match, so every disambiguated label fell through to Scripture. 70 story sets have been typed as Scripture for seven ticks: Bhojpuri, Maithili and 68 more told a reader they had a translation when they have 50 illustrated narratives. Found only because the type counts moved when this tick changed the test to a prefix. An exact match against a label that can grow a suffix is the whole defect, and the fix is not enough on its own — so clause H now asserts that nothing named "Open Bible Stories" is typed as Scripture, poisoned with the rest. 8/8 clauses fail on demand. The Door43 chip in the sheet had the same exact-match and now reads "Bible Stories (OBS-TLF) · Door43 · <scope>". The one `===` left in the fetcher is correct and was checked rather than blanket-replaced: it compares the RAW subject from the API, which never carries an owner suffix. Every Scripture row on the map now has a scope. The 2,278 blanks reconcile exactly to 2,087 film plus 191 Bible Stories — none of which is a portion of the canon. Verified: gate passes, its self-test fails 8/8 on demand, and the Door43, pins, table, scope and deep-link probes all pass. |
||
|
|
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.
|
||
|
|
2484f26304 |
facet gate: say "I cannot measure this engine", not "the map is broken"
Some checks are pending
build-gate / build (push) Waiting to run
Run against ?provider=maplibre the gate reported
FAIL — the map never populated (no rd-bubbles features after 90s)
about a dashboard that visibly renders all 2107 dots with a working
choropleth. It reads the source's live GeoJSON off `_data`, a private Mapbox
internal. MEASURED on one dashboard, one build, only the engine differing,
by calling the app's own setData() to shrink 2107 features to 5:
mapbox _data 2107 -> 5 _options.data 2107 -> 2107 (_data is LIVE)
maplibre _data never an FC _options.data 2107 -> 2107 (NEITHER is live)
So on MapLibre no private field tracks setData and the wait can never
succeed. It now diagnoses the engine and SKIPs with the reason. Same
false-FAIL shape removed from check:filter-width last tick: a control that
accuses correct code is worse than no control.
★ DELIBERATELY THE SMALL FIX. Re-expressing the assertion on the public
querySourceFeatures() would change the gate's reported counts, and the two
earlier attempts at that made things worse -- one failed BOTH engines by
preferring the construction-time _options.data, the other called a
node-side helper from inside a page-evaluated string where it is simply
undefined. Every line of page code added here is self-contained for exactly
that reason.
THREE CONTROLS:
maplibre -> SKIP, exit 0, naming why it cannot count
mapbox -> PASS, exit 0
--self-test -> gate REJECTED the reverted build, catching
'circle-color is the flat literal "#ef6c00"'
The third is the one that matters: the gate still catches the bug it exists
for rather than having been quietened.
This removes the LIE, not the GAP. MapLibre dashboards remain unverified by
this gate; BUG-FACET-GATE-CANNOT-MEASURE-MAPLIBRE stays open, downgraded to
P3 and re-scoped to the coverage question.
|
||
|
|
f03b900ccc |
filter-width: an absent server is not a width regression
The gate printed "FAIL — 2 host(s)" when it had measured NOTHING —
ERR_CONNECTION_REFUSED on :4173, "UNMEASURED" twice, then a failure. That
accuses the product of a regression on the strength of a missing process,
and it already cost one wrong report in this flight: the gate was reported
as failing when it had simply never run.
★ THIS DOES NOT REVERSE THE AUTHOR'S RULE. "An unmeasured host is a failure.
A gate that quietly drops a host reports success about something it never
looked at" is correct for a STRUCTURAL miss — the page loads and the trigger
or tab cannot be found, meaning the gate has been defeated — and that path
is kept, comment and all. Only connection-level errors are separated out.
It is also not a judgement call, because the house already decided: four
sibling browser gates skip on an unreachable host and say so in the same
words — check-fits-the-screen, interop-harness, check-catalog-identity and
check-legend-matches-source all print "SKIP — ... unreachable right now.
Not a product finding." This gate was the outlier.
THREE CONTROLS, ALL FIRED:
no server -> SKIP, exit 0
server + healthy code -> PASS, exit 0
real regression, the original
flex RANGE put back on
.filter-dropdown -> FAIL, exit 1 ("the row grew 369px")
The third is the one that matters: it proves the gate can still catch the
bug it exists for, rather than having been quietened.
Before believing any of those browser measurements I ran `bun run tree` and
confirmed check:sri 0 — a single-bundle build leaves stale SRI, the browser
refuses the script, and the page then measures clean for the wrong reason.
That trap made a negative control vacuous earlier in this flight.
|
||
|
|
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.
|
||
|
|
7b27bf1b83 |
Every link https, and a gate so it stays that way
1,237 of the URLs we ship were plain http — all of eBible.org, png.bible and alkitab.pw, with the host spelled `eBible.org` on six of them. Every one of those hosts serves https directly (sampled 12/12 at 200) and redirects http there anyway, so the plain link bought nothing but a redirect hop and a downgraded scheme in a field worker's browser, on a page that may itself be embedded over https. NORMALISED WHERE A URL ENTERS THE INDEX, not per fetcher. Doing it in the eBible fetcher first left 120 links behind — the curated scripture_url values, which fill-never-overwrite preserves and which therefore never pass through a fetcher at all. One httpsify at every emit point takes it to 3,548 https and 0 http. Scheme and HOST only. The path is left exactly as published, because a path is case-sensitive and "tidying" it would break the link rather than improve it. ★ THE GATE CAUGHT MY OWN DRIFT, ten seconds after I added the clause. Clause G failed — but as clause E, "1237 table URL(s) exist in no pin record": I had rebuilt the pins and not the table, so the two surfaces disagreed about every eBible link in the corpus. That is precisely the class this gate was written for last tick, and it found it on its first real use rather than three ticks later by hand. Clause G is poisoned in the self-test alongside the rest: 7/7 clauses now fail on demand. Verified: 0 plain-http URLs remain, six upgraded links sampled live all answer 200 over https, the coverage gate passes, and pins, table, deep-link, tooltip and list probes all still pass. |
||
|
|
0fffb90cca |
test(i18n): ratchet down again — seven locales must now stay complete
Some checks are pending
build-gate / build (push) Waiting to run
344 -> 282 keys. de and hi reached zero and join ar/es/fr/pt/ru as entries DELETED from the DEBT map: the gate treats an absent locale as "must be complete", so removal is what makes a finished translation permanent. The three that remain (it, ro, zh) are each missing WHOLE catalog files rather than stray keys, which is worth stating in the file — it tells the next person these are real translation work, not the propagation gaps the last two ticks closed. Confirmed the guarantee before relying on it: deleting one Hindi tab key reports "hi is missing 1/176 key(s) and has no DEBT entry" rather than passing. |
||
|
|
b4902a7fed |
test(i18n): ratchet down — five locales now must stay complete
Some checks are pending
build-gate / build (push) Waiting to run
The ceilings drop as the debt is paid: 429 -> 344 keys, and ar/es/fr/pt/ru reached zero. Those five are REMOVED from the DEBT map rather than set to 0. The gate already treats a locale with no entry as "must be complete", so deletion is what converts a finished translation into a permanent guarantee — setting 0 would read the same but leaves a line inviting someone to raise it. Confirmed the guarantee works before relying on it: deleting one Spanish key reports "es is missing 1/176 key(s) and has no DEBT entry" rather than passing. |