rega (7a7ca67b): on the dev server the corner widget never mounted, because /__plugin-gallery/manifest.json
built its profiles without the .meta.json fields only generate-tree read. Both now read them through one
module, internal/profile-meta.mjs (stage, heavy, widget per profile; bundle stage = production if any
profile says so), so the dev Studio and the built Studio cannot disagree. Verified: the dev model lists the
same 7 production bundles and rudder-voice:bottom-right; the built manifest is unchanged.
rega measured the Arabic packet (row 83) and found page 8 still English. dx-shepherd's rule, which this
follows exactly: NEVER machine-translate Scripture — use a published public-domain translation per locale,
cited with its name (END-DXS-MK5GO).
ar Arabic Van Dyck Bible ru Russian Synodal Bible es Reina Valera 1909
de German Luther Bible 1912 fr French Ostervald Bible
Fetched from bible.helloao.org, which ships licenceUrl + website + sha256 per translation and points at
ebible.org's own licence pages. Full provenance — translation id, name, licence URL, sha256 — is recorded in
data/people-groups/scripture-sources.json, because a credit line on the page can carry a NAME but not a
verifiable source, and the two are different claims.
★ VERSE NUMBERING VERIFIED AGAINST THE ENGLISH, not assumed. All five extract exactly verses 1-20 of 43 in
the chapter; verse 1 matches the WEB's "they came to the other side of the sea, into the country of the
Gadarenes" and verse 20 ends at "Decapolis ... everyone marveled". Lengths land within 1,689-2,399 chars
against the English 2,141, which is the spread you would expect across languages rather than a truncation.
The English passage carries no verse numbers, so these are joined as continuous prose the same way.
⚠ AND I DID NOT WRITE ANY OF IT FROM MEMORY. Scripture reproduced from recall is machine translation with
extra confidence — the same failure the rule forbids, one step removed. Every character came from the API
and was checked against the English before storing.
hi/zh/pt/it/ro STAY ENGLISH and stay in DECLARED_ABSENT, which is now narrowed rather than deleted: PD
candidates exist (Hindi 1851 Serampore-lineage, Chinese Union Version 1919, Almeida 1819, Diodati 1649,
Cornilescu 1924) but their licence status varies by jurisdiction and I could not confirm it. The entry
shrinks one locale at a time as each is cited.
VERIFIED: check:packet-strings-parity PASS, and the ar and de packets both render 9 sheets PASS with the
passage and its credit on page 8.
BUILD-CORNER-WIDGET-CLASS-RUDDER-IN-PLUGIN-STUDIO (design approved by dx-shepherd). A profile's .meta.json
declares "widget": { "corner": "bottom-right" | ... }; generate-tree carries it into manifest.json, so the
Studio keeps no list. The Studio mounts each widget once in a fixed corner host (z-index above everything),
outside the rail and the pane, collapsed to a button, with its open state remembered. Picking it in the rail
opens the corner instead of mounting a second copy in a pane. The host and the mounted element live in a
page-level singleton on document.body, so a Studio hot update or remount re-attaches the same element and
Rudder keeps its live stream. rudder-voice declares bottom-right. Needs eyes: open, HMR, collapse.
Rudder is the first plugin that isn't a map: it shows that one plugin-designs/ folder, developed live
in the Plugin Studio, can ship as a desktop app with its own local backend.
- plugin-designs/rudder-voice: <rudder-voice> element. Words stream into the end of a text area while
you type/paste anywhere (typing holds the stream; nothing duplicated or lost). Mic picker with
hot-plug, 3-second mic test, plain-language errors, info panel, privacy-safe debug report.
meta.json declares "targets": ["desktop"].
- backend/rudder-engine (Go): supervises NVIDIA NeMo-Speech.cpp (ggml, CPU) with the Nemotron 3.5 ASR
Streaming GGUF over a loopback WebSocket; records with parec/pw-record only while recording; recovers
dead Bluetooth mic links (reopen -> CVSD voice profile -> optional RUDDER_BT_RESET) and starts
headsets on CVSD. No telemetry: traced with strace, zero sockets/DNS while transcribing.
- Privacy: log lines redact MAC addresses and the home folder, transcripts are never logged, Host check
against DNS rebinding, CORS only for the desktop webview, random API key for the speech server.
- internal/backend-dev-plugin.mjs + vite.config.js: `bun run dev` starts built plugin backends and
proxies /api/<name> same-origin, so a plugin calls the same API in the studio and on desktop.
- backend/rudder-engine/setup-runtime.sh: fetches the runtime and model pinned by SHA-256.
Not yet: the Tauri desktop build (sidecar) and the PocketBase word-correction table.
The gate caught what I shipped: rendering the Tibet packet at PACKET_LANG=ar failed with "the funnel page
carries Latin-script prose in a ar packet: 'This packet is', 'UPG-', 'lists … How each number is reached'".
Exactly right — my new strings were English-only, and two of them PRINT on a page whose siblings
(funnel_title, funnel_intro, funnel_people) are fully translated. English text on an otherwise Arabic page
is not a graceful fallback, it is a half-translated page.
So all 17 new keys are now genuinely translated across es fr pt de it ro ru ar hi zh — 150 values — and
DECLARED_ABSENT is back to its original two entries (the Mark 5 passage and its credit). The declaration
mechanism was the right call while they were untranslated and the wrong place to leave them.
⚠ 'UPG-' IS ALLOWLISTED, AND THE ORDER MATTERS. It is the Latin run left by "UPG-100", the product name every
locale keeps verbatim — the same reason 'Project' is already in LATIN_OK. But it is allowed only AFTER the
real translations landed: an allowlist entry added when the failure first appeared would have silenced the
gate on fifteen genuinely untranslated strings. The allowlist is for names, not for work not yet done.
VERIFIED at the rendered layer in BOTH locales: en and ar each PASS with 9 sheets, map painted, content
intact. The Arabic packet carries its life-calendar title, christian-share title, both legend labels, the
three prayer columns and the "1 of 28" tip in Arabic, with 2 grids and 32 shaded cells — the same numbers as
the English one, which is the point: the arithmetic is language-independent and only the words change.
check:packet-strings-parity PASS.
Folding eight en-US call sites onto library/utils/formatCount.js took the matrix from 13 warns to 3,
and that looked like progress. It was not: the rule matches a LITERAL 'en-US', the helper calls
toLocaleString(COUNT_LOCALE), so my eight sites went INVISIBLE while the behaviour was untouched — the
same false-green as a precondition that skips the file under test. The audit had stopped seeing a
decision that is still in force.
The rule now also matches the constant's DEFINITION. One site, attributed to every bundle that imports
it, which is what "held in one place" should read as: 5 warns —
library/utils/formatCount.js:26 against research-dashboard and people-groups-grid
RegionCountryMap.vue:394 regions' held Driver decision
pplr-coverage.vue:128, pplr-pins.vue:60
regions' catch-fallback discrimination is untouched and its self-test stays green (rc=0).
Gate only, no product code, no rebuild.
generate-tree's preload comment now ends "...the map draws with MapLibre and this line must go", but the
gate still looked for "remove this line", so under MapLibre it kept the preload and every arm reported a
Mapbox request that no real MapLibre host makes (rega, 1173a992). It matches either wording now.
Driver, 2026-09-27, in his own page order. Nine pages now, was seven.
1 map the zone's vertical one-pager (unchanged)
2 LIFE CALENDAR 100 years x 52 weeks = 5,200 boxes — a whole human life on one page
3 THE SAME GRID shaded by the evangelical share of these groups' POPULATION
4 funnel now an UPSIDE-DOWN TRIANGLE with an equation sidebar and a "1 of 28" tip
5..n groups 20 per page with photos AND three prayer tick columns
then the 12 prayer points and the prayer wheel (both unchanged)
★ THE 12 PRAYER POINTS WERE NOT LOST. The Driver believed they had gone; all 12 are intact in
packet-strings.json with his rhythms and sub-titles — praise/Two-Cross Gospel, wait/Holy Spirit &
deliverance, confess, read/the Father's love, ask/baptisms, intercede/laborers & shepherds,
pray_word/Persons & Houses of Peace, thank/rest, sing, meditate/communion, listen/accountability,
praise_end/great-greater-greatest — plus the Mark 5 passage and the Metacamp attribution.
★ PAGES 2 AND 3 ARE ONE GRID READ TWICE, which is the whole idea. 5,200 is the Driver's own arithmetic
(100 x 52), not a layout choice, so both pages derive from it and cannot drift into two grids that merely
look alike. A percentage drawn as a bar is an abstraction; drawn on the grid the reader has just
understood as their own life, 32 shaded boxes of 5,200 is the same fact made unavoidable.
★ AND PAGE 3's NUMBER IS POPULATION-WEIGHTED, because he asked for "% of Christian population vs lost".
That matters: by GROUP COUNT 69.1% of the 2,091 matched DOXA groups have no known evangelicals, but by
POPULATION the evangelical share is 0.61% of 209,079,159 people. The group-count version is a materially
milder picture of the same data. Computed in data/people-groups/christian-share.json, joined IMB
CSV ROP3Code+ISOAlpha3 to DOXA's snapshot — the same join the Vision Card uses — and READ here, never
re-derived, so one fact has one source.
⚠ THE BAND MIDPOINT IS AN ASSUMPTION AND THE PAGE SAYS SO. peoplegroups.org publishes EvangelicalLevel
as a BAND ("Less than 2%", "No Known Evangelicals"), never a number, so any percentage requires choosing
a value inside each band. The file records the method, the page prints it, and 0.61 + 99.39 = 100 exactly
— my first formatter rounded the large side to "99%", which loses the asymmetry that IS the point and
leaves a reader checking the arithmetic to find the page wrong.
⚠ GEOMETRY MEASURED, NOT EYEBALLED: square cells would need 26.5px x 100 rows = 2,654px of grid in
1,521px of printable height, overflowing the sheet 1.7x. 13px rows put 100 rows in 1,498px with 23px
spare. Caught by computing the height before rendering.
⚠ AND THE TIP SAID "1 of 27". listCount spread the three cluster keys and omitted `deaf`, whose two
deaf_lists entries buildLegendNodes collapses into one leaf — the same omission allZones() had before
3ef164c0, and this was its last copy. It prints on the page, so a 27 there is a visible wrong fact.
THE GATE IS TAUGHT, NOT TOLERANT: the two new sheets are DETECTED (.sheet-life/.sheet-christian) and
added as known terms, exactly as the funnel was, because that assertion exists to catch silently dropped
people groups and a ±1 tolerance is the slack a lost list page hides in. New assertions: 5,200 cells per
grid, the shaded count neither 0 nor 5,200 (either means the percentage never arrived), and exactly
3 x cards prayer boxes. Probes scoped to .lc-grid — the legend swatches share the cell class and made the
count 5,201 on the first run.
The 17 new strings are English-only and DECLARED_ABSENT with reasons rather than back-filled: English
copied into ten locale blocks would read as translated to this gate and to a reader, and the per-key
fallback already renders English without the lie.
VERIFIED: check-packet-render PASS (9 sheets, map painted, content intact) and
check:packet-strings-parity PASS on the Tibet fixture.
Both halves of studio's L13U refusal were correct and both are mine.
1. THE LEGACY upg100WallIntro FALLBACK IS GONE. It existed for exactly one window — the three
plain-text keys were landing in studio's catalogue while this consumer was already written, so the
branch rendered the old markup-bearing key until they arrived. They are on main in all eleven
locales, so the condition can no longer be true: it is DEAD CODE that keeps a key alive, because
studio cannot delete upg100WallIntro while a line still reads it. A fallback whose condition cannot
fire is not safety, it is a reference.
The three remaining mentions in this file are COMMENTS, not code — verified that no live call site
anywhere in plugin-designs/ or library/ reads it, so it is now safe to delete.
2. THE GATE DEFAULTED TO A GITIGNORED FILE. check:zoning-declared read
library/scripts/posters/out/_records.json, which .gitignore:94 excludes, so on a clean checkout it
exited 75 — NOT MEASURED forever, indistinguishable from an unwired gate. studio measured exactly
that and correctly refused to wire it into gates:post. This tree has been here before:
check:packets-all SKIPped on every build for weeks for the same reason.
★ THE FIX IS A COMMITTED FIXTURE, NOT A LOOSER GATE. data/people-groups/zoning-records.json carries
slug plus every field a selector actually reads — 2,107 records, ~300 KB. country_code alone would
not do: the deaf zone reads rop1 and five African/European zones read wagf_block/wagf_region, which
is the same omission that made six zones silently resolve to nothing in the packet generator.
The live cache is still PREFERRED when present, so a local run measures the freshest data and CI
measures the committed one.
VERIFIED on a genuinely clean tree (git archive of the staged tree, no cache, no network):
PASS — 28 zones, 2,107 records, 2,096 covered, all five assertions green, exit 0.
check:i18n-keys-exist PASS, check:sfc-parse PASS.
The gate failed on main 12a64702 with two LATE_KNOWN entries that no longer match anything:
dashboard-plus-grid.vue#reflectDir and research-map-plus-grid.vue#reflectDir. Landing 11 fixed both —
reflectDir now captures the instance in setup, so a host switching lang to ar after mount does get
dir=rtl — and the exemptions stayed behind.
That is the gate working as designed: it refuses an exemption that has stopped describing anything,
because a stale entry is a defect it would quietly stop reporting. Both entries are deleted, and the
note left in their place says why rather than leaving a bare gap.
Gate only, no product code, no rebuild. composite-interop rc=0 after.
bp-big-picture could not be measured "because there's no canvas under a minimal config", and that was
the honest answer to a question it should never have been asked. Big Picture nests NO MAP: it composes
a research header and a people-groups grid, and its built bundle contains zero occurrences of
mapbox-gl or maplibre-gl. There is no canvas to draw and no Mapbox half to prove.
The target list was wrong, not the gate. A profile qualifies if `composes` names a map bundle OR the
type is nested-elements-mfe; that second arm exists because country-posters-mfe bakes its children and
names nothing in `composes`, but it is loose enough to admit composites that nest no map at all.
Now a non-`composes` candidate must also SHIP an engine, decided on the BUILT artefact rather than the
manifest, because whether a bundle compiled a map engine is a fact about what it built. Map-less
composites are printed as N/A so they stay visible instead of being silently dropped.
MEASURED: exactly 2 excluded — bp-big-picture (big-picture) and stack-demo — both genuinely map-less.
No other owner's map target is affected, so the set goes from 9 targets with 2 permanently unmeasured
to 7 real targets plus 2 declared N/A.
Gate only, no product code, no rebuild. Still needs a browser and a token to run: rega's, not mine.
country-posters-mfe now reads profile-config view (exp-shepherd, 66837586); it opens on posters, so the gate
passes view:"map" to have a canvas to measure. Browser gate: rega confirms the unmeasured cases drop.
generate-tree's asset allow-list staged people-groups-slim and -points but not people-groups/lists/*.json,
so on the built tree every non-default list (frontier-500k, frontier, unreached, unengaged) 404'd and the map
drew 0 pins (go-int / rega at a71007f1). The lists folder now ships whole, like tables/: the fetch name is the
list id. Verified on a regenerated tree: all four files staged; check:list-prop-unit PASS.
The Driver: a public plugin that is not yet a production map shows under Experimental until it is. A
profile's .meta.json declares "stage": "production" | "experimental" (default experimental; a bundle is
production if any profile says so) and "heavy": true for a live WebGL owner. generate-tree carries both into
manifest.json, so the Studio and the gallery read the same field and neither keeps a list.
The seven production bundles are marked: dashboard-plus-grid, research-map-plus-grid, regions-dashboard,
my-upg-100-list, doxa-simple-map, cities-internet, gospel-resources. The Studio rail keeps its capability
sections for them and puts every experimental bundle (gospel-internet among them) in one final Experimental
section. The gallery's auto-discovered Experimental folder skips a production bundle. gospel-internet-board
joins HEAVY_ELEMENTS (it can own two WebGL contexts). check:studio-rail asserts the split.
A media query adds no specificity. The mobile block's
`.map-toolbar { gap: 6px }` is (0,1,0) exactly like
`.map-toolbar--square { gap: 0 }`, and it comes later in the sheet, so it
won: on every phone a square toolbar fell back to separate buttons inside
one ringed panel. The shape broke precisely where the Driver is most likely
to look at it.
The --no-geocoder pair is (0,2,0) and outranked the variant whatever the
order, so it needs the two-class answer.
This is the same lesson as the goal cards' gap-vs-margin: a ported mechanism
is only ported if it still WINS, and specificity is the part that decides
whether it does. Found by following the phone thread rather than by looking
at the variant again.
check:square-map-controls now asserts the override is inside the mobile
block, that the two-class form answers --no-geocoder, and -- as a control --
that the base rule really does come first, which is why the override is
needed at all. Removing it turns the gate red.
The CSS comment explaining it cost one more mistake: I wrote the selectors
in backticks, inside the template literal that holds the stylesheet. A
backtick ends the literal, which is the trap check:sfc-parse names in its own
failure text, and it caught me with it.
THE MATRIX WAS UNDER-REPORTING, AND MY OWN KIT LANDING IS WHY. This gate scanned
plugin-designs/<bundle>/** and nothing else, which was correct while every panel lived inside its
plugin. The dashboard-kit extraction moves panels into library/components/dashboards/ and the bundle
still COMPILES them, so a source rule stopped firing the moment the file moved, with the code
unchanged. Measured: kit landing 4c-3 moved DashboardFacetRail.vue, which holds three
toLocaleString('en-US') calls, and the matrix went from 13 warns to 10. Nothing improved; the audit
stopped looking. LOC-ENUS has no `shipped` matcher, so the built-bundle pass could not catch them
either.
This is not three lines. Every kit extraction, and every future one, silently shrinks this audit —
the more code becomes shared, the less of it this gate sees. So each bundle is now audited over the
library files it actually IMPORTS, resolved from its own `@map/...` specifiers and followed
transitively. Attribution stays per-bundle, which is what keeps the matrix actionable: a library file
compiled into four bundles is reported against all four, because all four ship it.
★ MY FIRST VERSION OF THIS WOULD HAVE TURNED THE BUILD RED WITH FOUR FALSE POSITIVES, and finding
that is the reason I measure a gate's blast radius before handing it over:
- library/api/sources.json got swept in and LIST-FETCH reported it three times as "a file that
fetches the list". It is the endpoint REGISTRY — a .json file declares endpoints and fetches
nothing. The per-bundle walk has an extension filter; my sweep did not. Now it does.
- library/api/DataSourceManager.js:464 was reported for LIST-DSM, on the line
`const dataSourceManager = new DataSourceManager()` — the singleton the module exports. That rule
exists to catch a CALLER constructing one without profileConfig. A module is not a bypass of its
own contract, and reporting it told three owners the library was broken because the gate read a
definition as a use.
AFTER, on main: 13 warns (was 10), 0 bypasses, PASS, and 19 bundles audited instead of 18 — following
imports gave one more bundle a row. Every restored warn is LOC-ENUS, which is warn-only by the
Driver's held numerals decision, so nothing here asks anyone to change a number.
Gate only. No product code, no rebuild. check:universal-props is not in gates:pre or gates:post, so
this cannot turn a build red on its own; it is run directly and by check:studio-all.
LOC-ENUS flagged any en-US format literal. Two of regions-dashboard's three
warns were the 'en-US' inside a CATCH, whose try formats with
`${locale}-u-nu-latn` -- a verbatim clone of DX-life's RegionDetail.vue:16,
Latin digits with the reader's grouping. The fallback exists because a bad
locale tag throws RangeError in toLocaleString and blanking the page is
worse than one wrong separator.
So the matrix's locale column said "ignores the locale" about code that
honours it, which is the kind of false warn that teaches people to stop
reading the column.
Measured before changing the rule: of the NINE LOC-ENUS sites in the tree,
exactly TWO are such fallbacks. The other seven are genuinely operative and
still flagged, so the tree goes 10 warns -> 8 and only those two move.
The exemption needs BOTH halves -- a catch AND a locale-derived primary --
because a lone catch must not excuse a format that never consulted the
locale. That is the one rule here that can widen by accident, so four
adversarial shapes are pinned as a self-test: the real pattern is skipped,
while a catch around a hardcoded primary, a catch whose primary never
consulted a locale, and a plain operative call are all still caught.
Loosening the exemption to a bare catch makes the self-test exit 3.
regions-dashboard keeps ONE warn: RegionCountryMap's popup, fixed en-US on
purpose, carded and evidenced by DX-life's own convention. I did not add an
ALLOW for it -- ALLOW is keyed per BUNDLE, so it would also blind regions to
a future operative en-US, and a visible warn on a decision already taken is
cheaper than a blind spot.
check:square-map-controls, registered with a gates:post token.
It reads node_modules/maplibre-gl/dist/maplibre-gl.css and compares our
ported values against THAT FILE rather than against a remembered number,
which is possible only because the look the Driver asked for is the engine
default: DX-life adds NavigationControl and overrides nothing, so the target
is published.
It asserts the three deliberate deviations as deviations -- the engine's
29px is not adopted, there is no overflow:hidden, and a forced-colors block
ships -- so each stays a stated choice rather than decaying into a mistake
nobody can see.
And it asserts the blast radius: round is the default and none of the nine
MapToolbar consumers passes controls=. Opting demo-map in turns it red,
which is what makes the silence mean something.
Its CSS assertions read the text passed to useShadowStyles, not the file.
The first run went red on "we do not adopt 29px" because the prop's own doc
comment quotes the engine's rule, 29px and all -- a checker that cannot tell
a comment from a declaration keeps finding the documentation it stands on.
key-cities now resolves its engine through the library (exp-shepherd, 71af822a, on main at c2784f5a), so the
'key-cities:ENG-STYLE' allowance is stale. Removed; the gate passes against main's key-cities (18 bundles),
and it fails on the pre-change file, so this commit must land on or after c2784f5a.
gospel-plugin's trap (COMMON.md): a CSS rule moved past the first argument of useShadowStyles concatenates
onto the style id ('@media(...){...}doxa-gospel-map'). That is valid JS, so the build stays green and the rule
is silently lost; the brace-balance scan never sees it because the id is not CSS. The walker now folds the
second argument too, and a literal id must match /^[a-z0-9-]+$/ (a computed id is left alone). Measured on
the current tree first: 0 offenders, so this breaks no owner. Self-tests cover the trap and a plain id.
From dx-shepherd (END-DXS-CSSID).
Six public/ conflicts, zero in source. They take main's side here and are regenerated from the merged
source in the next commit — a bundle is an output, and a hand-reconciled middle version is wrong by
construction.
generate-library-index took the first line of every file as its description, which for a .json file is data:
the Data section read '"_meta": {', '"value": "AFG",' and a run of ISO codes (flagged by simpleresearch).
A .json file now uses _description, then _meta.description, then _meta.purpose, and otherwise says
'(no description)'. iso3-to-alpha2 now reads as what it is.
A page's own reduced-motion reset never crosses a shadow boundary, so library components rendered inside
custom elements slid, faded and spun for readers who asked for less motion: thirteen of them, including the
legends and map controls (regions-plugin found it on ProgressBar and named the class).
useShadowStyles now injects one prefers-reduced-motion reset into each shadow root it styles, which covers
every component styled through it, including later ones. PosterDialog, FilterDropdown and SearchFilterHeader
style themselves with <style> blocks and carry their own guard.
check:shadow-reduced-motion (in gates:pre): useShadowStyles must inject the reset, and every animating
library component must be covered. It fails without the reset (negative-tested) and found the three
<style>-block components on its first run.
Behind any login gate (basic auth, a session cookie, OIDC) the gallery said "Could not load Big Picture":
doxa-maps-spa fetched its same-origin manifest, and plugin-studio its token URL, with credentials:'omit', so
the gate's cookie never went and the request was refused (found on the new dev container; dx-shepherd).
Both now use 'same-origin', which never sends credentials cross-origin, so CDN and CORS behaviour is
unchanged.
check:no-credentials-omit (+ :self-test, in gates:pre) fails any credentials:'omit' in library/ or
plugin-designs/ unless the line says why (// credentials-omit: <reason>). Measured before: exactly these two
sites; it fails on the old code and passes on the fix.
Arm C catches the live RTL defect in the two plus-grid composites: a top-level function that reads
getCurrentInstance() and is handed to a watcher or an addEventListener runs with no current instance,
so the read returns null.
IT MODELS VUE RATHER THAN BANNING THE CALL. Inside setup, and inside a lifecycle-hook callback, the
call is CORRECT — Vue sets the instance for the duration of a hook — so research-map-plus-grid.vue's
onMounted(() => { getCurrentInstance()... }) is deliberately not reported. A gate that flags working
code gets ignored, and this one has already had to drop five such hits.
★ AND ITS OWN EARLY-OUT MADE IT USELESS FIRST, WHICH IS THE SECOND TIME TODAY FOR THIS EXACT HOLE.
The file filter only ran the arms on sources containing `immediate: true` or `async` — arm A's and
arm B's triggers. Neither of the two files carrying the RTL bug contains either word, so both were
skipped before a single arm ran, and the gate reported the tree clean while the self-test stayed
green. A fixture is text and never meets the file filter, so no self-test could have caught it; only
running the gate against the un-fixed code did, which is why dx-shepherd asked for exactly that.
An hour earlier I diagnosed the same shape in check:i18n-keys-exist — it skipped any file that never
wrote the word "tStatic", which is how a planted poison key passed — and wrote up the rule that a
cheap early-out is part of a gate's COVERAGE, not an optimisation. Then I wrote the hole again here.
So the fix is structural rather than a correction: the precondition is now DERIVED from a per-arm
ARM_TRIGGERS table, so adding a fourth arm forces you to extend the filter in the same edit.
One more false positive the self-test caught before it shipped: `watch(textDir, reflectDir)` has no
inline callback, so brace-matching forward from the call walked past it into the NEXT statement's
onMounted body and reported a getCurrentInstance() nowhere near a watcher. A call's callback is now
bounded by that call's own parenthesis span.
Verified both directions, on the real files and not only on fixtures: FAILS on the shipped code
naming both composites, PASSES with the fix, and does not flag the legal onMounted read.
Gate only, no product code, no rebuild — rule 5B because it lives in internal/.
check:sfc-parse proves a .vue script block parses. A broken stylesheet
inside a valid string literal is still a valid string literal, so it
passed a sheet with 9 declarations stranded outside any rule and braces
at 12 open / 21 close. Nothing else in the tree validated CSS syntax.
check:shadow-css-parses reads every useShadowStyles() argument and scans
the result as CSS: brace balance, declarations stranded at depth 0, empty
selectors. 72 stylesheets across library/ and plugin-designs/.
It reads the arguments off the @babel/parser AST the build already uses,
because three hand-rolled lexers each produced a confident FALSE RED on a
file that was fine: a /* CSS comment */ read as a stranded declaration; an
apostrophe in a .vue prose header opening a phantom string literal, so a
</div> from the template was reported as CSS; and a regex literal
containing a quote, which cannot be lexed without resolving regex-vs-
division. The CSS scan itself stays hand-written, because CSS is the thing
being checked -- and it consumes CSS comments and quoted values as their
own token types, so content:'a{b' opens no rule.
9 self-tests run inline on every invocation, including the exact damage
above, so the gate cannot quietly stop being able to fail. An argument
built partly from an imported constant is scanned for its literal
segments and labelled, rather than skipped -- skipping it would have
excluded region-dashboards.vue, which builds its sheet from the token
layer plus literals.
JSON.parse keeps the last of two equal keys and says nothing, so a duplicate silently kills the first entry
while every gate that reads the parsed object passes. "check:religion-label-not-value" sat twice in
//scripts after a merge (found by regions-plugin; only Bun's parser warned). A small scanner tracks keys
per object; its self-test runs on every invocation, and a planted duplicate is caught. In gates:pre.
The landing tooling's package.json union now refuses to write a result with a repeated key.
useDashboardAggregations has a split personality, and only half of it could ever
move. `lbl` and `aggregate` are pure counting — eleven lines, a Map, a sort, a
topN rollup, no facet name and no colour. Everything else in that file is
research: `colorFor` with its hardcoded religion / wagf_region / affinity_bloc
branches, the religion palettes, the donut and bar builders. Those stay, and
DashboardChartsRail keeps importing them.
The generic pair was what blocked kit landing 4c: the facet rail needs only that
half, and while it lived beside the palettes the rail could not follow it into the
library. Same split, same one-cycle re-export as 4a. Bodies verbatim.
★ AND THE ROLLUP BUCKET SAID "Other" IN EVERY LOCALE. aggregate() pushes
`{ key: 'Other', other: true }` for the tail, and that English word was rendered
directly by the facet rail and the charts rail. check:i18n-keys-exist could not
see it, because there was no T() call to inspect — the string was never a lookup,
it was a KEY being used as a LABEL. I found it while ASSESSING this landing rather
than while doing it, and it is fixed here rather than later for one reason: the
moment this module becomes kit code, an unfixed bug in it is exported to every
dashboard built from the kit.
THE TWO HALVES PULL IN OPPOSITE DIRECTIONS, which is the whole difficulty. The KEY
must stay English: the rail tests it to make the row inert, and the charts rail
refuses to filter on it, so a localized key would make the bucket CLICKABLE in
Arabic and filter on a value no row carries — an empty board, reading as a data
problem. The LABEL must not stay English. So OTHER_KEY is the sentinel,
otherLabel() reads dashboard.other, and they live next to the function that
produces the bucket so they cannot drift. Eleven locales, catalogue first.
This is the value/label trap for the third time — religion families, the language
tiers, and now a bucket in generic code. The third is the worst place for it,
which is why the rule now travels WITH the code that creates the value rather than
being remembered at each render site.
check:legend-mirrors grows arm 4, asserting both directions: translate the
sentinel and it fails; render the key instead of the label and it fails. Both
mutants run.
check-one-palette now scans the new module as well. Its subject — private palette
declarations — stayed in the plugin half, so nothing moved; adding the library
path means a palette smuggled into kit code later is caught by the same rule
instead of escaping into the library.
Library index regenerated in-commit. Source only. STUDIO: needs a rebuild of
research-dashboard and dashboard-plus-grid. The internal/ hunk is four lines in
check-one-palette.mjs.
Both came from the Driver's console in the gallery's Simple Research tab, and both come from one
gap: where a line SITS in a setup block is not WHEN it runs.
research-map.vue — "Cannot access '_lastFitKey' before initialization", thrown during setup, which
left the map blank. The externalFilters watcher is immediate:true, so it runs while setup is still
on the stack and calls applyFitSlugs() 350 lines below it. The CALL resolved, because a function
declaration is hoisted; its constants are not, so the first read hit their temporal dead zone. A
hoisted function promises that its NAME resolves early, not that its BODY can run early, and that
asymmetry is invisible from both ends: the watcher looks like it only touches the store, and the
constants look like they sit right beside the function that reads them. They and the guard now sit
above the watcher, with a note saying why they cannot live next to applyFitSlugs().
DashboardMapPanel.vue — "onBeforeUnmount is called when there is no active component instance".
A lifecycle registrar closes over Vue's current instance, which exists only while setup is on the
stack, so a hook registered after "await initializeMap()" is silently dropped: a ResizeObserver on
document.body plus a window resize listener outlived the component. Nothing threw and the feature
worked, so it showed only as a studio that slowed down the more tabs a reader had visited. The fix
is not to move the line — the code that creates those handles is past the await either way. The
handles move to setup scope and are torn down in the hook this file already registers correctly,
which is the pattern it was using three lines away for containerRO and attribRO.
research-map.vue held a second copy of the same leak, its scroll-zoom resync listener. The new
gate found that one; reading the file had not.
check:setup-order covers both classes. Its own false positives are the instructive part: 6 hits
down to 1 over three rounds, and every one it dropped was prose or shadowing read as code — an
onBeforeUnmount inside a console.error STRING, the word "table" inside a new Error(...) template
literal in four files, and a destructured byCountry shadowing a computed 220 lines away. So it
blanks comments, string text and template text while keeping ${...}, which is code, and subtracts
every name a span declares for itself. It self-tests on both shipped defects, both fixes and all
three decoys, and exits 3 if an arm goes blind rather than passing quietly.
Source only. doxa-research-map, research-dashboard and dashboard-plus-grid need a rebuild.
check:no-silent-engine is in gates:post as of main 23976fb0 and scans every plugin for
a cross-engine fallback, so this leaf's own static arm is deleted. Two gates owning one
source-level rule meant my leaf was checked twice while every other leaf was checked
once, and the narrower copy is always the one that drifts.
What stays is the part no source read can do: two providers across engine present,
engine blocked and both blocked, with the CDN blocked by request routing the way a proxy
or an offline kiosk does it, and the engine identified from the canvas class rather than
a debug handle. All six still pass after the deletion.
--poison now NAMES where the falsifiable arm went instead of exiting 0 as though it had
self-tested — and it names it correctly: the tree-wide gate's flag is --self-test, not
--poison, which I checked in its source rather than assuming the convention my own gates
use. Its package.json description no longer claims a static arm this file does not have.
Part 1 of making UPG-100 a reusable pattern (Driver via repo; dx-shepherd UPGPAT). Any project list —
7000 / 4700 / 3000 / 2000 / 700 — will be zoned by an agent who did not write this code. Zoning is human
judgement (geography first, balanced with language), so a gate cannot decide whether a zoning is GOOD. It
can decide whether it still matches what the author SAID, which turns "did I zone this right?" from a
browser question into an exit code.
★ EVERY OBVIOUS RULE I WOULD HAVE WRITTEN FAILED THE REFERENCE IMPLEMENTATION. Measured against the live
28 zones and 2,107 records BEFORE writing the assertions, which is the only reason they are right:
"every group in exactly one zone" → 112 groups are in two. 110 are the DEAF BLOC, and that is
design-approved (legend-tree.js:151, slice 08): an affinity bloc's
members also live in a geographic zone. So the rule is "at most one
GEOGRAPHIC zone", with bloc zones declared as a second partition.
"no group is unzoned" → 11 are, all JPN/CHN/MNG, plausibly deliberate. A gate cannot tell a
deliberate exclusion from a silent drop; only a declaration can.
"no two zones overlap" → 3 non-bloc overlaps exist, because "Scattered / Diaspora"
(country_codes AND,CCK,GUF,USA) overlaps the regions it scatters
across.
A gate that fails on the only correct example of the thing it checks teaches people to disable it.
⚠ AND THE GATE FOUND A THIRD OVERLAP MY OWN HAND ANALYSIS HAD MISSED: deaf-cocos-islanders, in both
oceania-indonesia and uncovered-scatter. I had filtered out every A017 group as "the design-approved deaf
double-count" — but its TWO GEOGRAPHIC claims (CCK is in both zones' country lists) are a SEPARATE fact
from its bloc membership. Conflating them is exactly what A2's bloc/geographic split exists to prevent,
and the gate was right where I was wrong. Best argument for it existing.
THE FIVE ASSERTIONS, and two would have caught this cycle's real defects before a browser did:
A1 no zone resolves to zero groups ← `deaf` + the 5 wagf zones were in exactly this state, found
only by generating all 28 and reading the log
A2 at most one GEOGRAPHIC zone/group ← blocs declared; found the overlap above
A3 orphan set EQUALS the declared set ← and a RESOLVED orphan fails too, deliberately: leaving a
now-zoned group listed means the next real drop passes
silently. An unmaintained declaration stops being evidence.
A4 every selector type is supported ← the evaluator THROWS on an unknown type, at render time, on one
zone's page; better as a build-time failure on the whole set
A5 no zone over the declared budget ← a wildly oversized zone is usually a selector matching too much
DECLARATIONS pre-filled with today's actuals and marked PENDING DRIVER, per UPGPAT: the 11 orphans and
the 3 overlaps are recorded so the gate is green on arrival and any CHANGE fails. Both are in
DRIVER-DECISIONS-DIGEST and neither membership was touched.
VERIFIED: --self-test PASSes 12 cases, each assertion firing on a violation AND staying quiet on the
declared exception (including the negative control that A2's bloc exemption is doing the work). Poison
test on REAL data: removing ainu-japan from the declaration makes A3 fail with that slug, so a green run
is meaningful rather than vacuous.
⚠ AND A package.json MISTAKE WORTH RECORDING. My first insertion put all three keys into the //scripts
DOC block, because re.finditer over the whole file matched check:zone-census there (//scripts comes later
in the file) — producing duplicate keys that json.load silently resolved last-wins, so the script COUNT
WENT DOWN and `npm run check:zoning-declared` reported "Missing script". Fixed by scoping each insertion
to its own block span. The diff is now exactly 3 added lines and reorders nothing: an earlier attempt that
re-serialised the sorted object moved gospel's check:embed-contract and check:engine-contract, which is
not mine to reshuffle in a shared file.
Seven consumers switch to the -web copies exp-shepherd built (5410842a: every feature and island kept, about
a fifth of the bytes): useCountryOutline, useMapLayers, regions RegionCountryMap, research-dashboard
DashboardMapPanel, cities-internet connectivity-map (world + India states), gospel resources-map (world +
India provinces) and my-upg-100-list (world + UPG-100's India partition; China unchanged). generate-tree
stages the three -web files beside the full ones. Posters, print and every build-time point-in-polygon keep
the full files, as REPORT-WEB-BOUNDARIES.md requires: no runtime code tests membership on these.
Six public/ conflicts, ZERO in source — checked before the lock rather than discovered inside it, and it
held: the only overlap between main and this branch is my own bundle, its three leaf HTMLs and the two
manifests, because main rebuilt them and so did I.
They take main's side HERE and are regenerated from the merged source in the next commit. A bundle is an
OUTPUT: the only correct content is whatever the build emits from the merged inputs, and any
hand-reconciled middle version is wrong by construction even when the diff looks plausible.
studio's landing 9 (19682d2c, carrying my ee0bf338 by identical blob 9bdc45a9) has not landed, so mine
goes in first and theirs becomes the no-op. Both orders were agreed safe with studio and dx-shepherd
because the content is byte-identical either way.
?codes= filters by LANGUAGE, which is what a partner's list usually is and the wrong
filter for a list of peoples. The reach table measured the gap in its own rows: its
Mandarin row counts 36 groups, those 36 speak 30 codes, and 55 Doxa groups speak those
30 — so a link from a 36-group row drew 55 pins. Akha's 33 drew 57; SUN's 93 drew 116.
The extra groups are real and also have nothing; they belong to a different row.
So the map takes ?groups=<slug list> and filters by people group. The slug is
doxa.life's own — carried from the roster into the pin rows, and read in the builder
off the prayer URL their API built from it, never constructed from a name, because a
slug this build invented could name somebody else's people. All 369 of those rows
carry a prayer URL, measured before relying on it.
Row and map now agree by construction: 0 of 35 rows disagree, and the Mandarin link
renders "36 / 2,107" on the map against a row claiming 36. The gate asserts exact
equality for a three-slug link and 0 for an unknown slug — the failure that would
quietly turn a partner's 40-group embed into the whole roster.
The longest link is 1,577 characters, SUN's 93 slugs; well inside what browsers carry,
but worth knowing before anyone adds a row with a thousand groups.
The Driver's console, P0: in Plugin Studio's gallery, switching tabs threw "SyntaxError: Unexpected error"
from createI18n in createAppI18n. vue-i18n's createGlobal() runs inside scope.run() and throws
UNEXPECTED_ERROR when that returns undefined; Vue 3.5 creates a new EffectScope INACTIVE when the current
one is stopped; and a custom element's connectedCallback -> configureApp -> createAppI18n ran while the
released tab's stopped scope was still current. Root cause traced by dx-shepherd.
The i18n instance and its locale watcher are now created in effectScope(true), which never inherits the
ambient scope. The install wrapper also makes a second provide('appI18n') of the same instance a no-op, so
the bundle entries that still provide it themselves stop warning "App already provides property".
check:i18n-stopped-scope (in gates:post): a bare createI18n() inside a stopped scope must throw (control),
createAppI18n() there must succeed, translate and keep its locale watcher live, and a second provide must
not warn. On the previous index.js it fails with the Driver's exact error.
dx-shepherd's ruling: the locale-honouring fallback everywhere, carrying the
Driver's goal (d) — a bare-host embed with ?lang=ar must render Arabic — now
recorded in COMMON.md as the embed contract.
all-people-groups passed its inline EN map as the fallback, which served ENGLISH to
exactly the hosts that cannot install vue-i18n: the seven bundles here that install
none. It now takes the catalogue default, so those readers get their own language
with English still the last resort. The EN map is deleted — with the catalogue
complete it could rescue nothing, and a second copy of the English is only a place
for the two to drift.
THE RULING'S CONDITION WAS MEASURED BEFORE THE SWITCH, not after: all eleven keys
exist in all eleven locales, and ar differs from en on 11 of 11, so the change is
observable rather than cosmetic.
★ AND THE CONDITION IS NOW KEPT TRUE RATHER THAN MEASURED ONCE. A hand census
protects the day it is taken. Section 9 of check:optional-t-equivalence re-derives
it every run, because with no local fallback an uncatalogued key renders as a raw
dotted path to a reader — the exact failure upg100 described, and the reason they
had advised keeping a fallback at all.
★ THAT CENSUS UNDER-COUNTED TWICE BEFORE IT WAS RIGHT, both caught by checking
instead of assuming:
- literal tr('key') extraction found 6 of 11. Four keys are category ids reached
only through tr(cat.id) / tr(c.id); the census now extracts the CATS ids too, and
asserts the dynamic call sites still look the way it assumes.
- the eleventh, 'pray', was lost to codeOnly: it sits in a template literal whose
`https://` was read as a line comment, because the scanner had already lost
template state further up a 1000-line SFC. The census now reads RAW source.
The asymmetry is the point — over-including a commented-out key makes the check
STRICTER (a cheap false red); under-including a real one makes it claim
completeness it does not have. strip-code.mjs carries that rule now, so the next
author picks raw or stripped for a reason rather than by habit.
rega, row 46 at 835ec1ac: on the prayer map neither the shared-point sentence nor the standing pin note
appeared. research-map and countries-map provide normalizedPeopleGroups to their legends; the prayer map
did not, so its legends fell back to dataStore's cache, which holds the RAW API rows: field names the
co-location index cannot read, and no GeoJSON geometry for the standing-note detector to find.
useMapData now provides the normalized rows it draws, so every map built on it offers them. LegendMobile
and SemanticTreeLegend read them first (the cache stays the fallback), and the detector accepts a row with a
name and a position as well as a GeoJSON point. check:approx-pin-note asserts the provide and the
normalized-row match; the provide check fails on the previous useMapData.
check:no-silent-engine reported gospel-resources/resources-map.vue as the last remaining offender at
main 9df086e5. It is not, and has not been since their landing: line 1513 reads
`const gl = engineGlobal(grProvider)` with no `||` at all. I relayed the red to dx-shepherd as fact
before looking at the line, and the line was PROSE.
★ WHAT IT WAS MATCHING. gospel documented what they removed, in a /* */ block whose second line begins
with a BACKTICK:
`engineGlobal(grProvider) || window.mapboxgl`. engineGlobal already defaults to mapbox, so
The skip asked only whether a line STARTS with //, * or /*. That is not what a comment is: it misses
every CONTINUATION line of a block comment whose text begins with something else. So the gate now
tracks block-comment state instead, and the --self-test carries gospel's exact shape as a regression
case — plus its inverse, a real offender after a closed block, because fixing a false positive by
skipping MORE is only correct if the true positives still fail. Both assert in the same run.
★ AND THIS IS THE WORST FAILURE MODE A GATE CAN HAVE, which is why it gets a commit of its own rather
than a quiet one-line patch. It told the one agent who did the right thing that they had done the wrong
thing, and the cheapest way to make it green was to DELETE the explanation — so the gate actively
attacked the reasoning it exists to protect. This file's own header quotes the bad pattern for exactly
that reason; the skip was there from the start and was simply too shallow to cover the form someone
else actually used.
⚠ AND IT MADE A STALE CARD LOOK LIVE. The offender's line number moved 1490 -> 1505 -> 1509 across
three landings purely because their file grew, so each re-read produced a plausible new number for a
defect that had already been fixed. A card that names file:line ages into a false claim silently; the
gate has to be the source of truth, and it was lying too.
Tree-wide result now: 6 files call engineGlobal(), PASS — every one keeps the engine its provider asked
for. The gate is finally green on a green tree, which means it can be wired into gates:post. That wiring
is shared config (rule 5B) so it is NOT in this commit: prepared for studio separately.
★ THE TWO INSTRUCTIONS I WAS GIVEN CONTRADICTED EACH OTHER ON ONE LINE, so this records how it was
resolved rather than leaving the next reader to guess. UPGHK said delete humanizeKey; UPGW3 said merge
1bd5f109 and "keep regions' version" — and regions' version KEEPS humanizeKey
(`useOptionalT('', { fallback: (k) => humanizeKey(k) })`). Read as structure-vs-fallback they agree:
UPGHK's reasoning is about what a reader SEES ("a real English or translated string beats a title-cased
key segment"), so "keep regions' version" is about the SHAPE of the call. Kept theirs in every respect
but the fallback:
- useOptionalT('') with the EMPTY namespace, theirs, not my 'messages'/'tabs'. Verified to be a
behavioural no-op here (every key this code passes is already fully qualified, so qualify() returns
it unchanged either way) — and '' is the more honest of two identical options, because it says
"these keys arrive qualified" instead of implying a prefix is being added.
- their "one behaviour gain" note, which is a real gain I had not documented: the old path was the
bare useI18n().t, so on a host WITH i18n a missing key rendered the raw dotted key.
- the fallback deleted, per UPGHK.
AND THE REGISTER KEEPS BOTH WAVES. Their side of the conflict added a wave-3d row for
all-people-groups that mine would have dropped — it stays, with its own reading intact, which is the
OPPOSITE of the call taken for my two files (its inline EN map stays because the file is UNOWNED and an
unowned file is the wrong place to make a product decision by default). Both readings are right for
their own file; that is the point.
⚠ AND I FIXED A FALSE SENTENCE THE GATE WAS ABOUT TO PRINT. With a null third column the register said
"takes the CATALOGUE fallback, as it always did" — true for the two composables that always did, false
for my two, which used humanizeKey until today. That branch already existed to stop exactly this for
gospel's four; my files needed the same treatment, so the "moved" list now names both owners and says
what each moved FROM. A register can be right about its assertion and still teach the next reader
something untrue in the line it prints, and a gate message is the one place a reader has no reason to
doubt.
Verified: check:optional-t-equivalence PASS on all sites, including gospel's unchanged message.
The rows studio removed when 4c05e589 was not in their landing go back in — but NOT as
regions-plugin wrote them. 4c05e589 had the third column as humanizeKey(k), which was correct for
the migration it was written against and became wrong the moment dx-shepherd took the product call
(UPGHK) to DELETE that fallback. A register row asserting a fallback the code no longer passes fails
by design, which is the same hole regions flagged to me in the opposite direction.
So both sites register with null: no fallback, resolve through the catalogue. The gate then asserts
!/fallback:/ rather than a fragment, which is exactly what the code now does.
Three defects in one report, all of them the kind that makes a number read as verified when it is not.
1. THE DENOMINATOR CAME FROM A DIFFERENT POPULATION THAN THE NUMERATOR. It counted
out/posters/*.html — POSTER files — while the numerator counts PACKET zones, and the two are not
the same set: 28 posters against 27 packet-able zones, because `deaf` had a poster and no
obtainable packet (fixed in 3ef164c0). So "2 of 28" was not 26 to go, it was 25 to go plus one
that no command could produce. Both sides now read the SAME seam — leafZones(buildLegendNodes(...)),
which is what the generator itself enumerates — so they cannot drift apart again.
2. out/posters IS ITSELF GENERATED, so on a clean checkout the denominator was null and the
shortfall warning silently vanished. The one line whose job was to stop an overclaim could not
fire precisely when nothing had been rendered at all. Its absence is now stated out loud.
3. THE COUNT AND THE LIST DISAGREED ON SIGHT — my own bug, caught by reading the output rather than
trusting it: "21 zone(s)" printed above a list of 26. zones[] holds every FILE stem, which
includes things that are not zones (the four fixture-* packets and the <zone>.frontier ?list=
variant), so it overcounted coverage against a real-zone denominator and made "7 of 28" out of
2 real zones — a 3.5x overstatement. Count and list are now one derived set, and the non-zone
packets are reported separately as what they are: coverage of the RENDERER, not of a zone's data
shape.
And the warning now NAMES the missing zones instead of counting them. "26 zone(s) have no packet"
tells a reader the size of the hole and not what to type, which is why nobody ever closed it; the
named list is what surfaced `deaf` as unreachable rather than merely ungenerated.
Coverage now measured at 22 of 28, every one PASSing with --expect-funnel, up from 2 of 28. The six
still open are named in the output (central-africa, deaf, east-africa, europe,
southern-africa-islands, west-africa) and all six need a records source carrying the selector fields
3ef164c0 added to the query.
Three findings, one thread: rule 4 says verify a URL before shipping it, and the
check I wrote for it yesterday was measuring one file.
1. THE TABLES PAGE WAS SHIPPING DEAD FORM. people-group-resources.json — the
"Resources by People Group" tab, which a reader clicks — carried 2,294 URL cells, of
which 1,412 were jesusfilm.org/watch/?languageId=<id>. That form is IGNORED by the
site: a real id and a bogus one render the same generic library page, which is why
the map migrated onto Arclight shareUrls. The migration never reached this table, so
the booleans beside those links were being refreshed while the links themselves went
stale for a year of builds. 1,417 film and 882 scripture URLs re-derived from the
index, which is authoritative for both and whose URLs are the verified ones.
2. TWO ARCLIGHT LANGUAGES, ONE ISO CODE — resolved, with evidence rather than a
ruling. jos was SKIPPED here, and the note said resolving it meant deciding whether
the code or the publisher's name was authoritative, "a data question for someone who
can check it". Checked: media-languages?iso3=jos returns 7138 "Jordanian Sign
Language" and 139080 "Lebanese Sign Language", and 7138's shareUrl redirects to
.../jordanian-sign-language.html while a bogus arc.gt code 404s. So the code stays
authoritative for the join and the NAME breaks the tie the code cannot. The old note
also had a fact wrong — it called Lebanese "a different language (lbs)", but lbs is
LIBYAN Sign Language and Lebanese has no code of its own, so this is two Levantine
varieties sharing one code, not a mis-filing. Three Deaf groups now reach the JESUS
Film in their own sign language through a link that resolves; 0 retired-form links
remain in the index.
3. THE GATE'S SCOPE WAS THE BUG. It walked the pin index alone, which is how item 1
hid from it. It now reads every table the SURFACES read, taken from the surfaces, and
resolves each URL against four records: the verification table, the org contacts, the
SUN pages (proved by content against a catch-all control) and doxa.life's own listing
for prayer pages, which cannot be proved by fetch because pray.doxa.life answers 200
for any slug. Provenance fields are excluded by path — a citation is not an offer.
8,445 links measured yesterday; 12,292 shipped links today, 0 unrecorded, 0 broken,
0 that no status code can prove.
Reading every STAGED file instead reported 6,115 unrecorded and 53 broken, all of
them correct behaviour: API endpoints in source fields, and build caches that still
hold the 53 share URLs the verifier proved dead — which is exactly why the pin
builder drops them. A gate that goes red on correct behaviour gets switched off.