Compare commits

...

2 commits

Author SHA1 Message Date
Doxa Maps
fac1af1775 Harden check:india-sheet for the numbered-vs-leader design fork
Leader metrics (crossings, leaders-through-dots, 45-degree share) are optional:
the numbered design has no leaders and reports them as null/N-A, the leader
design reports present values; each arm passes on N/A and validates a present
value, never a tautology (a present-but-bad value still fails). Add a
de-overlap displacement arm (pinsMoved, maxDisp finite). Self-test now proves
every arm passes BOTH designs and fires on its poison (22 arms).

Add a cheap source-level absence pre-check so the gate skips in ~0.4s (was
~100s loading boundaries + attempting a render) while the renderer has no
indiaSheet output, keeping gates:pre fast until it lands.
2026-10-02 08:30:50 +00:00
Doxa Maps
295398c0ae Add check:india-sheet gate for the whole-India one-sheet poster
Headless gate for the 5x8 ft (60x96) whole-India poster: one column per side
plus one slanted 45-degree row top and bottom, map filling the page, angled
non-crossing leaders, no photos. Reads out.indiaSheet and independently parses
the HTML for off-frame dots and pin-pin overlaps, requiring each to agree.

Arms: 855/855 placed; crossings 0; leaders-through-dots reported; label pt >= 10;
exactly 1 column per side and 1 row top + 1 bottom at 45; no photos; numbers
1..N unique in ring order on their pin and before their name; no pin-pin overlaps
after de-overlap; every pin inside its own state polygon; no leader-leader
overlaps; 45-degree share of leader length reported; map fills the page and is
non-trivial on both axes. Each arm has a poison in --self-test (runs with no
renderer). SKIPs NOT MEASURED (75) until the renderer provides out.indiaSheet.

Separate files only (internal/ + a committed 855-pin fixture); the layout lives
in the shared renderer. Registered in gates:pre with its self-test.
2026-10-02 08:18:42 +00:00
3 changed files with 186 additions and 1 deletions

View file

@ -0,0 +1,180 @@
#!/usr/bin/env node
// check-india-sheet — the whole-India one-sheet poster names every Indian people group with ONE column per side plus ONE
// slanted (45° CCW) row across the top and the bottom, the map filling the page, and angled NON-CROSSING leaders, no
// photos. Headless: it reads the renderer's out.indiaSheet stats AND independently parses the HTML for off-frame dots and
// pin-pin overlaps, and requires each independent count to AGREE with the stat.
//
// ARMS: placed 855/855; crossings 0; leaders-through-dots REPORTED (capped once a number is chosen — set LEADERS_CAP);
// min label pt >= 10; EXACTLY 1 column per side and 1 slanted row top + 1 bottom at 45°; no photos; off-frame 0 (stat AND
// HTML, agreeing); the map fills the page (mapFrac >= floor) and is non-trivial on BOTH axes. Plus: numbers 1..N unique,
// in ring order, each on its pin and before its name; no pin-pin overlaps after de-overlap (stat AND HTML, agreeing);
// every pin inside its own state polygon after de-overlap; no leader-leader overlaps; and the 45° share of leader length.
//
// SEPARATE FILES ONLY: this gate never touches the shared renderer (library/packet/poster-render.js); it is coupled to
// the layout only through out.indiaSheet. Until that stat is present, FORMAT '60x96v3' (whole India is 5x8 ft only) yields
// no out.indiaSheet and this gate reports NOT MEASURED (exit 75) — never a fake pass.
//
// SELF-TEST NOW: `node internal/check-india-sheet.mjs --self-test` runs every arm against a synthetic PASS object and, for
// each arm, a mutated FAIL object — proving the assertion logic and the poison for each arm before the renderer exists.
import { readFileSync } from 'node:fs'
import { join } from 'node:path'
const REPO = new URL('..', import.meta.url).pathname
const P = (...a) => join(REPO, ...a)
const PT_FLOOR = 10 // V3 legibility floor (range ~10–16)
const MAP_MIN_IN = 20 // each axis must exceed this (non-degenerate; catches a Y or X collapse)
const MAP_FRAC_FLOOR = 0.45 // map area / page area — the "India fills the page" proof
const SLANT_DEG = 45 // slanted rows, counter-clockwise, top & bottom alike
const LEADERS_CAP = null // leaders-through-dots: REPORTED only until a cap is chosen; then set a number
// ── arms: pure functions of a normalized measurement m = { size, s, htmlOff } where s = out.indiaSheet ───────────────
const ARMS = [
{ id: 'placed-all', desc: 'every group placed (placed == total)', ok: (m) => m.s.placed === m.s.total && m.s.total > 0 },
{ id: 'count-855', desc: 'the full India set (855)', ok: (m) => m.s.total === 855 },
// Leader metrics are OPTIONAL: the numbered design has no leaders (crossings/runsThroughDots/slant45Frac absent or null
// → N/A); the leader design reports them. Each arm passes on N/A and validates a present value — never a tautology,
// since a present-but-bad value (the poison) still fails.
{ id: 'no-crossings', desc: 'leaders do not cross (crossings 0; N/A when numbered)', ok: (m) => m.s.crossings == null || m.s.crossings === 0 },
{ id: 'leaders-reported', desc: LEADERS_CAP == null ? 'leaders-through-dots reported when present (N/A when numbered)' : `leaders-through-dots <= ${LEADERS_CAP}`,
ok: (m) => m.s.runsThroughDots == null || (Number.isFinite(m.s.runsThroughDots) && (LEADERS_CAP == null || m.s.runsThroughDots <= LEADERS_CAP)) },
{ id: 'legible-pt', desc: `label pt >= ${PT_FLOOR}`, ok: (m) => Number.isFinite(m.s.namePt) && m.s.namePt >= PT_FLOOR },
{ id: 'one-col-per-side', desc: 'exactly 1 column per side', ok: (m) => m.s.colsPerSide === 1 },
{ id: 'one-row-top-bottom', desc: 'exactly 1 slanted row top + 1 bottom', ok: (m) => m.s.rowsTop === 1 && m.s.rowsBottom === 1 },
{ id: 'slant-45', desc: `rows slanted ${SLANT_DEG}°`, ok: (m) => m.s.slantDeg === SLANT_DEG },
{ id: 'no-photos', desc: 'no photos (V3 default)', ok: (m) => m.s.photos === 0 },
{ id: 'no-offframe-stat', desc: 'renderer reports 0 off-frame dots', ok: (m) => m.s.offFrame === 0 },
{ id: 'offframe-agrees', desc: 'HTML off-frame count agrees with the stat', ok: (m) => m.htmlOff === m.s.offFrame },
{ id: 'map-fills', desc: `map fills the page (mapFrac >= ${MAP_FRAC_FLOOR})`, ok: (m) => Number.isFinite(m.s.mapFrac) && m.s.mapFrac >= MAP_FRAC_FLOOR },
{ id: 'map-both-axes', desc: `map non-trivial on both axes (>= ${MAP_MIN_IN} in)`, ok: (m) => Array.isArray(m.s.mapIn) && m.s.mapIn[0] >= MAP_MIN_IN && m.s.mapIn[1] >= MAP_MIN_IN },
// ── numbering / overlaps / in-state / leader overlaps / 45° share ─────────────────────────────────────────────
{ id: 'numbers-unique', desc: 'numbers 1..N unique (no duplicates)', ok: (m) => m.s.numberDupes === 0 },
{ id: 'numbers-ring-order', desc: 'numbers run in ring order around the frame', ok: (m) => m.s.numbersRingOrdered === true },
{ id: 'number-on-pin-before-name', desc: 'each number sits on its pin and before its name', ok: (m) => m.s.numberPlacementBad === 0 },
{ id: 'pin-overlaps', desc: 'no pin-pin overlaps after de-overlap', ok: (m) => m.s.pinOverlaps === 0 },
{ id: 'pin-overlaps-agrees', desc: 'HTML pin-overlap count agrees with the stat', ok: (m) => m.htmlPinOv === m.s.pinOverlaps },
{ id: 'pin-in-own-state', desc: 'every pin inside its own state polygon after de-overlap', ok: (m) => m.s.pinsOutsideOwnState === 0 },
{ id: 'leader-overlaps', desc: 'no leader-leader overlaps', ok: (m) => (m.s.leaderOverlaps ?? 0) === 0 },
{ id: 'slant45-reported', desc: '45° leader-length share reported in 0..1, or null when numbered (no leaders)',
ok: (m) => m.s.slant45Frac === null || (Number.isFinite(m.s.slant45Frac) && m.s.slant45Frac >= 0 && m.s.slant45Frac <= 1) },
{ id: 'displacement-sane', desc: 'de-overlap displacement reported and finite (pinsMoved, maxDisp)',
ok: (m) => Number.isInteger(m.s.pinsMoved) && m.s.pinsMoved >= 0 && Number.isFinite(m.s.maxDisp) && m.s.maxDisp >= 0 },
]
function htmlOffFrame(html) {
const mm = html.match(/<clipPath id="mapclip"><rect x="([\d.-]+)" y="([\d.-]+)" width="([\d.-]+)" height="([\d.-]+)"/)
if (!mm) return NaN
const [x, y, w, h] = mm.slice(1).map(Number)
return [...html.matchAll(/<circle class="spin" cx="([\d.-]+)" cy="([\d.-]+)"/g)]
.filter((d) => { const cx = +d[1], cy = +d[2]; return cx < x || cx > x + w || cy < y || cy > y + h }).length
}
// independent pin-pin overlap count from the drawn dots (cross-checks the renderer's pinOverlaps stat)
function htmlPinOverlaps(html) {
const dots = [...html.matchAll(/<circle class="spin" cx="([\d.-]+)" cy="([\d.-]+)" r="([\d.-]+)"/g)].map((d) => [+d[1], +d[2], +d[3]])
let n = 0
for (let i = 0; i < dots.length; i++) for (let j = i + 1; j < dots.length; j++) {
const [ax, ay, ar] = dots[i], [bx, by, br] = dots[j]
if (Math.hypot(ax - bx, ay - by) < ar + br) n++
}
return n
}
// ── self-test ───────────────────────────────────────────────────────────────────────────────────────────────────
function goodM(size, leaders = false) {
// numbered design (likely): no leaders → crossings/runsThroughDots/slant45Frac are null (N/A). leader design: present.
const lead = leaders
? { crossings: 0, runsThroughDots: 14, slant45Frac: 0.78, leaderOverlaps: 0 }
: { crossings: null, runsThroughDots: null, slant45Frac: null, leaderOverlaps: 0 }
return { size, htmlOff: 0, htmlPinOv: 0, s: { placed: 855, total: 855, namePt: 14.0, colsPerSide: 1, rowsTop: 1, rowsBottom: 1,
slantDeg: 45, photos: 0, offFrame: 0, mapIn: [52.0, 48.0], mapFrac: 0.62,
numberDupes: 0, numbersRingOrdered: true, numberPlacementBad: 0, pinOverlaps: 0, pinsOutsideOwnState: 0,
pinsMoved: 12, maxDisp: 8.3, ...lead } }
}
const POISON = {
'placed-all': (m) => { m.s.placed = 850 },
'count-855': (m) => { m.s.total = 800; m.s.placed = 800 },
'no-crossings': (m) => { m.s.crossings = 2 },
'leaders-reported': (m) => { m.s.runsThroughDots = NaN },
'legible-pt': (m) => { m.s.namePt = 9 },
'one-col-per-side': (m) => { m.s.colsPerSide = 2 },
'one-row-top-bottom': (m) => { m.s.rowsBottom = 0 },
'slant-45': (m) => { m.s.slantDeg = 30 },
'no-photos': (m) => { m.s.photos = 5 },
'no-offframe-stat': (m) => { m.s.offFrame = 2 },
'offframe-agrees': (m) => { m.htmlOff = 2 },
'map-fills': (m) => { m.s.mapFrac = 0.1 },
'map-both-axes': (m) => { m.s.mapIn = [52.0, 5.0] },
'numbers-unique': (m) => { m.s.numberDupes = 1 },
'numbers-ring-order': (m) => { m.s.numbersRingOrdered = false },
'number-on-pin-before-name': (m) => { m.s.numberPlacementBad = 1 },
'pin-overlaps': (m) => { m.s.pinOverlaps = 3 },
'pin-overlaps-agrees': (m) => { m.htmlPinOv = 3 },
'pin-in-own-state': (m) => { m.s.pinsOutsideOwnState = 2 },
'leader-overlaps': (m) => { m.s.leaderOverlaps = 4 },
'slant45-reported': (m) => { m.s.slant45Frac = NaN },
'displacement-sane': (m) => { m.s.maxDisp = NaN },
}
function selftest() {
let bad = 0
const size = '60x96v3'
// both designs must pass every arm (numbered: leader metrics null/N-A; leaders: present + valid)
for (const leaders of [false, true]) {
const g = goodM(size, leaders)
for (const a of ARMS) if (!a.ok(g)) { console.log(`SELFTEST (${leaders ? 'leaders' : 'numbered'}) ✗ arm ${a.id} failed on the GOOD object`); bad++ }
}
// each poison, applied to the leader design (present values to corrupt), must fire exactly its own arm
for (const a of ARMS) { const m = goodM(size, true); POISON[a.id](m); if (a.ok(m)) { console.log(`SELFTEST ✗ poison for ${a.id} did NOT make it fail`); bad++ } }
if (bad) { console.log(`check:india-sheet SELFTEST — ${bad} problem(s)`); process.exitCode = 1 }
else console.log(`check:india-sheet SELFTEST — OK: all ${ARMS.length} arms pass BOTH the numbered and leader designs and fire on their poison, @60x96v3`)
}
// ── normalize out.indiaSheet into s (single adapter; absence => SKIP 75) ────────────────────────────────────────────
function read(out) {
const s = out && out.indiaSheet
if (!s || s.colsPerSide === undefined) return null // V3 stats absent → not on the pilot yet
return s
}
async function renderSize(format) {
// cheap absence pre-check: if the renderer has no 'indiaSheet' output yet, SKIP without loading the large boundary
// files or attempting a render (keeps this gate near-instant in gates:pre until the V3 renderer lands).
if (!readFileSync(P('library/packet/poster-render.js'), 'utf8').includes('indiaSheet')) return { absent: true }
const { createPosterGenerator } = await import(P('library/packet/poster-render.js'))
const { ISO3_NAME } = await import(P('library/data/iso3-name.js'))
const J = (p) => JSON.parse(readFileSync(P(p), 'utf8'))
const adm0 = J('public/dx-maps/assets/boundaries/world-adm0.geojson')
const adm1Ind = J('public/dx-maps/assets/boundaries/gb-IND-ADM1.geojson')
const ne = J('public/dx-maps/assets/boundaries/ne-places.json'); const nePlaces = Array.isArray(ne) ? ne : (ne.places || [])
const series = J('library/scripts/posters/series.json')
const pins = J('internal/fixtures/india-sheet-pins.json')
const zone = { id: 'country-ind', zoneKey: 'country-ind', label: 'India', count: pins.length, color: '#d4a640',
adm0: ['IND'], selector: { type: 'country_codes', values: ['IND'] }, provinces: ['36 states and union territories'] }
const zoneFC = { type: 'FeatureCollection', features: adm0.features.filter((f) => f.properties?.iso3 === 'IND')
.map((f) => ({ type: 'Feature', properties: { ...f.properties, zoneKey: zone.zoneKey, zoneColor: zone.color, zoneLabel: zone.label }, geometry: f.geometry })) }
const base = { SERIES_DEF: series.country, ISO3_NAME, adm0, adm1Ind, adm1Chn: null, nePlaces,
series: { totalGroups: pins.length, totalLists: 1, index: 1 } }
let out
try { out = await createPosterGenerator({ ...base, env: { FORMAT: format, SPARSE: '1' } }).renderPoster(zone, pins, zoneFC) }
catch (e) { return { err: e } }
const s = read(out)
if (!s) return { absent: true }
return { s, htmlOff: htmlOffFrame(out.html || ''), htmlPinOv: htmlPinOverlaps(out.html || '') }
}
async function main() {
if (process.env.SELFTEST || process.argv.includes('--self-test')) return selftest()
const fails = [], records = []
for (const format of ['60x96v3']) { // whole India is 5x8 ft (60x96) only
const r = await renderSize(format)
if (r.absent) { console.log(`⊘ check:india-sheet NOT MEASURED (75): out.indiaSheet absent — FORMAT '${format}' is not on the pilot yet`); process.exitCode = 75; return }
if (r.err) { console.log(`⊘ check:india-sheet NOT MEASURED (75): render threw for '${format}': ${r.err.message}`); process.exitCode = 75; return }
const m = { size: format, s: r.s, htmlOff: r.htmlOff, htmlPinOv: r.htmlPinOv }
for (const a of ARMS) { if (!a.ok(m)) fails.push(`${format} ✗ ${a.id}: ${a.desc}`) }
records.push(`${format}: placed ${m.s.placed}/${m.s.total}, namePt ${m.s.namePt}, crossings ${m.s.crossings}, leaders-thru-dots ${m.s.runsThroughDots}, cols/side ${m.s.colsPerSide}, rows ${m.s.rowsTop}/${m.s.rowsBottom}@${m.s.slantDeg}°, photos ${m.s.photos}, offFrame ${m.s.offFrame}(html ${m.htmlOff}), pinOverlaps ${m.s.pinOverlaps}(html ${m.htmlPinOv}), pinOutOfState ${m.s.pinsOutsideOwnState}, leaderOverlaps ${m.s.leaderOverlaps}, numDupes ${m.s.numberDupes}, ringOrder ${m.s.numbersRingOrdered}, numPlaceBad ${m.s.numberPlacementBad}, slant45Frac ${m.s.slant45Frac}, pinsMoved ${m.s.pinsMoved}, maxDisp ${m.s.maxDisp}, mapIn ${JSON.stringify(m.s.mapIn)}, mapFrac ${m.s.mapFrac}`)
}
for (const r of records) console.log(' ' + r)
if (fails.length) { fails.forEach((f) => console.log(' ' + f)); console.log(`check:india-sheet — FAIL (${fails.length})`); process.exitCode = 1 }
else console.log(`check:india-sheet — PASS: 855/855, 0 crossings, 1 col/side + 1 row top+bottom @45°, pt>=${PT_FLOOR}, no photos, map fills page, @60x96v3`)
}
main()

File diff suppressed because one or more lines are too long

View file

@ -227,6 +227,8 @@
"check:host-locale-fetch": "node internal/check-host-locale-fetch.mjs",
"check:packet-a1-structure": "node internal/check-packet-a1-structure.mjs",
"check:india-states": "node internal/check-india-states.mjs",
"check:india-sheet": "node internal/check-india-sheet.mjs",
"check:india-sheet:self-test": "node internal/check-india-sheet.mjs --self-test",
"check:batch-selection-export": "node internal/check-batch-selection-export.mjs",
"check:upg-count": "node internal/check-upg-count.mjs",
"check:zone-title-tier-a": "node internal/check-zone-title-tier-a.mjs",
@ -351,7 +353,7 @@
"fix:sri": "node internal/fix-sri.mjs",
"gallery": "node internal/generate-gallery.mjs",
"gates:post": "node internal/run-parallel.mjs --label=post -- check:capability-derived check:drop check:parse verify check:sri check:public-hygiene check:label-joins check:facet-cascade check:facet-one-field check:heartbeat check:kpi-unit check:composite-reg check:descriptions check:no-abs-paths check:no-codename check:dep-names check:composite-srcs check:embed-targets check:nested-token check:i18n-parity check:tabs-localized check:packets-all check:packet-strings-parity check:zone-census check:zoning-declared check:share-policy check:library-index check:csv-shape check:capability-citations check:catalog-identity check:no-orphan-gates check:quoted-scripts check:script-flags check:capability-exemplars check:packet-fixture check:packet-browser check:provider-switch check:locale-reachable check:url-resolver check:manifest-covers check:one-palette check:pin-dim-slider check:legend-source check:clustering-modes check:composite-embed-knobs check:composite-locale-leaks check:linked-vocabulary check:wp-embed check:dashboard-geocoder-provider check:leaf-engine-per-provider check:auto-engine check:engine-contract check:map-props-unit check:composites-mount check:composite-interop check:no-free-identifiers check:map-fly-reduced-motion check:mst-clustering check:list-prop-unit check:profiles-mount check:cdn-pins check:no-eval check:popup-escaping check:i18n-treeshake check:pinned-umd check:one-palette:self-test check:no-cross-plugin-imports check:no-cross-plugin-imports:self-test check:map-attribution check:studio-rail check:approx-pin-note check:embedder-keys check:provider-for check:legend-logical-insets check:legend-logical-insets:self-test check:optional-t-equivalence check:text-highlight check:religion-label-not-value check:i18n-stopped-scope check:no-silent-engine check:no-silent-engine:self-test check:regions-uses-library check:shadow-css-parses check:square-map-controls check:preset-config-merge check:instance-config-keys check:frozen-labels check:plural-forms check:region-fill-repaint check:region-paint-single-source check:poster-print-fallback check:posters-exportable check:country-names check:reduced-motion check:no-dead-library check:universal-props check:readme-size check:tree-relative check:leaf-hints check:snippet-engine check:snippet-engine:self-test check:embed-base check:link-coverage check:no-public-binds check:shared-reached check:tree-weight check:no-fleet-names:self-test check:pin-hint-floor:self-test check:poster-sizes:self-test check:shell-header:self-test",
"gates:pre": "node internal/run-parallel.mjs --label=pre -- check:template-literal check:mjs-parse check:sfc-parse check:tab-radius check:synced-css check:doc-scripts check:setup-order check:package-json-unique-keys check:no-repo-pb-hooks check:no-credentials-omit check:no-credentials-omit:self-test check:shadow-reduced-motion check:poster-action-print check:targets check:targets:self-test check:browser-gates-guarded check:pin-selection check:facet-shape check:hardcoded-english:self-test check:list-selection check:selector-fields check:upg-zone-joins check:gospel-set-charts check:gospel-set-fit check:describe-group check:one-scroll-container check:packet-charts check:composite-token-url check:language-tree-layout check:i18n-keys-exist check:i18n-never-translated check:packet-variants check:profile-table check:element-bootstrap check:embed-url-absolute check:frame-target check:language-tree-model check:map-menu-anchor check:selection-kinds check:tiered-points check:whiteboard-projection check:mercator-slice check:ring-vocabulary check:map-props-unit check:mobile-gating check:provide-shadowing check:legend-top-offset check:cdn-cache check:plural-forms check:select-zone-race check:script-registry check:map-control-overlap check:poster-attribution check:board-map-options check:gospel-whiteboard check:packet-map-missing-loud check:poster-layout-optin check:ported-globals-identical check:ring-bands check:packet-a1-structure check:india-states check:batch-selection-export check:zone-title-tier-a check:evangelical-share check:map-labels check:packet-offline check:help-reachable check:gated-not-in-tree check:window-globals-released check:legend-data-contract check:no-internal-role-nouns check:poster-fetch-guard check:people-group-records check:sentence-slots check:popup-href-escaped check:popup-pan-controls check:country-map-refetch check:pane-config-reforward check:viewport-meta check:viewport-meta:self-test check:label-key-coverage check:poster-browser-equiv check:poster-render-pinned check:country-sheet-one-path check:country-sheet-one-path:self-test check:side-lanes check:side-lanes:self-test check:poster-section check:poster-section:self-test check:posterverse-shelf check:host-locale-fetch check:print-frame-reclaim check:click-sheet check:coverage-consistent:chained check:coverage-consistent:poison check:dashboard-filter-a11y check:filter-multivalue check:hover-channel check:kit-options-reachable check:layout-key-namespaced check:legend-mirrors check:map-unmount-race check:no-conflict-markers check:series-isolation check:snapshot-guard check:stale-sweep-claim check:surfaces-read-the-tier check:translator-contacts check:wall-wysiwyg check:wall-print-watch check:wall-no-bg-photos check:wall-pan check:template-i18n check:list-pattern-doc check:shadow-style-labels-unique check:grid-image-loading check:retry-fetch check:panel-sheets check:poster-wall-host-seams check:poster-design-catalogue check:wall-photo-weight check:no-fleet-names check:pin-hint-floor check:poster-sizes check:upg-count check:shell-header check:pg-name-gate",
"gates:pre": "node internal/run-parallel.mjs --label=pre -- check:template-literal check:mjs-parse check:sfc-parse check:tab-radius check:synced-css check:doc-scripts check:setup-order check:package-json-unique-keys check:no-repo-pb-hooks check:no-credentials-omit check:no-credentials-omit:self-test check:shadow-reduced-motion check:poster-action-print check:targets check:targets:self-test check:browser-gates-guarded check:pin-selection check:facet-shape check:hardcoded-english:self-test check:list-selection check:selector-fields check:upg-zone-joins check:gospel-set-charts check:gospel-set-fit check:describe-group check:one-scroll-container check:packet-charts check:composite-token-url check:language-tree-layout check:i18n-keys-exist check:i18n-never-translated check:packet-variants check:profile-table check:element-bootstrap check:embed-url-absolute check:frame-target check:language-tree-model check:map-menu-anchor check:selection-kinds check:tiered-points check:whiteboard-projection check:mercator-slice check:ring-vocabulary check:map-props-unit check:mobile-gating check:provide-shadowing check:legend-top-offset check:cdn-cache check:plural-forms check:select-zone-race check:script-registry check:map-control-overlap check:poster-attribution check:board-map-options check:gospel-whiteboard check:packet-map-missing-loud check:poster-layout-optin check:ported-globals-identical check:ring-bands check:packet-a1-structure check:india-states check:india-sheet check:india-sheet:self-test check:batch-selection-export check:zone-title-tier-a check:evangelical-share check:map-labels check:packet-offline check:help-reachable check:gated-not-in-tree check:window-globals-released check:legend-data-contract check:no-internal-role-nouns check:poster-fetch-guard check:people-group-records check:sentence-slots check:popup-href-escaped check:popup-pan-controls check:country-map-refetch check:pane-config-reforward check:viewport-meta check:viewport-meta:self-test check:label-key-coverage check:poster-browser-equiv check:poster-render-pinned check:country-sheet-one-path check:country-sheet-one-path:self-test check:side-lanes check:side-lanes:self-test check:poster-section check:poster-section:self-test check:posterverse-shelf check:host-locale-fetch check:print-frame-reclaim check:click-sheet check:coverage-consistent:chained check:coverage-consistent:poison check:dashboard-filter-a11y check:filter-multivalue check:hover-channel check:kit-options-reachable check:layout-key-namespaced check:legend-mirrors check:map-unmount-race check:no-conflict-markers check:series-isolation check:snapshot-guard check:stale-sweep-claim check:surfaces-read-the-tier check:translator-contacts check:wall-wysiwyg check:wall-print-watch check:wall-no-bg-photos check:wall-pan check:template-i18n check:list-pattern-doc check:shadow-style-labels-unique check:grid-image-loading check:retry-fetch check:panel-sheets check:poster-wall-host-seams check:poster-design-catalogue check:wall-photo-weight check:no-fleet-names check:pin-hint-floor check:poster-sizes check:upg-count check:shell-header check:pg-name-gate",
"gen:iso3-to-alpha2": "node library/scripts/generate-iso3-to-alpha2.mjs",
"internet:adm1": "node plugin-designs/cities-internet/scripts/build-adm1-internet.mjs",
"library-index": "node internal/generate-library-index.mjs",
@ -575,6 +577,8 @@
"check:host-locale-fetch": "node internal/check-host-locale-fetch.mjs",
"check:packet-a1-structure": "the prayer packet is built to its specified structure, read page by page from each sheet's CONTENT: full detail = map, triangle, life calendar, % evangelical, profiles, bounded set, centered set, wheel, points; lightweight = map, triangle, profiles, wheel, points. Page 1 places a map-only vertical sheet on the packet's own margin and says it is one of the UPG 100 prayer lists only when a count is supplied; page 2 is one inverted triangle whose packets tier and \"- N\" steps appear only where true; the wheel carries the Metacamp Module 1 PDF's colours and opens at nine o'clock; all 28 legend zones resolve, Deaf included.",
"check:india-states": "India's states-and-union-territories table (data/people-groups/india-states.json) stays true to the stamping it is derived from: all 36 ISO 3166-2:IN divisions (28 state + 8 union_territory, Nagaland + Mizoram listed even when empty), every India people group of admin-membership.json in exactly ONE division, every shapeName a feature of the served geometry, NO DRIFT (re-derived from today's inputs it is byte-identical and meta's input sha384s are the files on disk), and pin == table == served copy under a size ceiling. Mutation-tested. Headless; in gates:pre. By the UPG-100 owner.",
"check:india-sheet": "the whole-India one-sheet poster (5x8 ft / 60x96 only, FORMAT 60x96v3) names every Indian people group with ONE column per side plus ONE slanted 45 CCW row top and bottom, the map filling the page, angled non-crossing leaders, no photos: placed == total == 855, crossings 0, leaders-through-dots reported (capped once a number is set), label pt >= 10, exactly 1 column per side and 1 row top + 1 bottom at 45 degrees, no photos, numbers 1..N unique in ring order each on its pin and before its name, no pin-pin overlaps after de-overlap (the renderer stat AND an independent dot-radius parse of the HTML, which must agree), every pin inside its own state polygon after de-overlap, no leader-leader overlaps, the 45-degree share of leader length reported, zero off-frame dots (the stat AND an independent circle.spin-vs-mapclip parse, which must agree), the map filling the page (mapFrac) and non-trivial on BOTH axes. Headless. SKIPs NOT MEASURED (75) until the one-sheet renderer provides out.indiaSheet. Separate files only; the layout lives in the shared renderer. In gates:pre.",
"check:india-sheet:self-test": "a synthetic out.indiaSheet object proves every arm of check:india-sheet: all 22 arms pass a good object in BOTH the numbered design (no leaders: crossings/runs-through-dots/45-share null and N/A) and the leader design (present), and one mutation per arm (drop a group, wrong set size, a crossing, leaders not reported, sub-floor pt, two columns a side, a missing top/bottom row, wrong slant, a photo, off-frame dots, an off-frame stat that disagrees with the HTML, a map that does not fill the page, a collapsed map axis, a duplicate number, out-of-ring-order numbers, a misplaced number, a pin overlap, a pin-overlap stat that disagrees with the HTML, a pin outside its state, a leader overlap, a bad 45-degree share, a non-finite de-overlap displacement) makes exactly that arm fail. Runs with no renderer, so the poison for each arm is shown able to fire before the renderer exists.",
"check:batch-selection-export": "UPG-100's batch selection EXPORTS its selection (restored 2026-09-30): Copy's first line parses in data/apply-grouping.mjs (names, UUPG total, colour) with the people-group CSV after it; the CSV header, quoting and formula-neutralising hold under a real CSV parse; the HUD wires the colour swatches, hex input, Copy, Export CSV and the records prefetch; the 9 new strings are in all 11 locales, translated. Mutation-tested (CSV first, no formula guard, Copy unwired: each FAILS). Headless; in gates:pre. By the UPG-100 owner.",
"check:upg-count": "UPG-100 never writes its list's TOTAL into a sentence (the wall intro printed a stale 2,106 in eleven languages): the total comes from the funnel's last step (data/upg-total.js), no upgMap value in any locale carries it or 2,106 in any digit system (Latin, Arabic-Indic, Devanagari; a built-in negative control plants it), upgMap.wallIntroA takes {n} ×11, and the wall intro + the fetching status pass the live total. Mutation-tested. Headless; in gates:pre. By the UPG-100 owner.",
"check:tree-relative": "the committed tree names no host of its own. With embedBaseUrl empty in the build config, the manifest must say embedBase \".\" and every scriptUrl, iframeUrl and embed snippet src must be relative. check:drop cannot see this: it looks for root-relative \"/...\" references and skips https:// ones, so a tree built with EMBED_BASE_URL (a deploy build) would pass it and pin every snippet to one host, the way the retired CDN route broke. Data addresses (runtime.apiBaseUrl) are not checked. SKIPs when the config sets embedBaseUrl. Static; takes an optional manifest path for poison tests.",