diff --git a/internal/check-pinned-fetch-fresh.mjs b/internal/check-pinned-fetch-fresh.mjs new file mode 100644 index 000000000..35ea7b0e2 --- /dev/null +++ b/internal/check-pinned-fetch-fresh.mjs @@ -0,0 +1,163 @@ +#!/usr/bin/env bun +/** + * check-pinned-fetch-fresh.mjs — a STALE cached copy of a pinned file never decides a print. + * + * THE DEFECT (2026-10-02, on a phone). The print-time files are pinned (poster-render.js, its classic copy, + * packet-strings.json, ne-places.json, india-states.json) and fetched with cache: 'force-cache', which returns ANY + * cached copy, however old. After a re-pin, a reader who had printed before kept the previous copy: it failed its pin + * on every print and the print fell back without a word. Page 1 became the stored poster (the phone's packet), a + * non-English packet would print in English, and India's state cards would be lost. library/packet/sha384.js + * fetchPinned fetches force-cache first and, on a copy that is not served or does not match, ONCE more with cache: + * 'reload'; every pinned loader calls it. + * + * A browser's HTTP cache is modelled: force-cache answers from it when it holds the URL, reload always asks the + * server and refreshes it. The SERVER holds the current files (public/dx-maps/assets); the CACHE holds a stale copy. + * A. fetchPinned: a stale cached copy → the current text, after exactly one fresh refetch; + * B. a cached copy that matches is used as is: ONE request, force-cache (the fast, offline-friendly path stays); + * C. a server that also serves the wrong bytes → rejected, err.integrity, after one fresh refetch; + * D. a file that is not served → rejected (not an integrity failure); + * E. loadPacketStrings with a stale cached table → the full table in every language (not the English fallback); + * F. loadIndiaStates with a stale cached table → India's 36 states (not a rejection); + * G. generateSheet with a stale cached generator → a drawn sheet (not an integrity refusal); + * H. wiring: every pinned loader fetches through fetchPinned with its own pin, and none fetches its pinned file + * directly; the classic-script path refreshes the HTTP cache and retries a refused tag once. + * `--self-test`: fetchPinned without its fresh refetch, one that never uses the cache, one that accepts a + * mismatch, and a loader that fetches its pinned file directly must each FAIL. + * Two-sided by hand at the change: the previous loaders (the pilot before this) fail E, F and G. + */ +import { readFileSync, existsSync } from 'node:fs' +import { join } from 'node:path' + +const REPO = new URL('..', import.meta.url).pathname +const ASSETS = join(REPO, 'public/dx-maps/assets') +const SHA_SRC = join(REPO, 'library/packet/sha384.js') +const LOADERS = { + 'library/packet/posterSheets.js': ['POSTER_RENDER_SHA384', 'NE_PLACES_SHA384'], + 'library/packet/packetStrings.js': ['PACKET_STRINGS_SHA384'], + 'library/data/indiaStates.js': ['INDIA_STATES_SHA384'], +} +const PINNED_FILES = ['lib/poster-render.js', 'lib/packet-strings.json', 'boundaries/ne-places.json', 'people-groups/india-states.json'] +const served = (p) => { const f = join(ASSETS, p); return existsSync(f) ? readFileSync(f, 'utf8') : null } +const asModule = (src) => import('data:text/javascript;base64,' + Buffer.from(src).toString('base64')) +const realFetch = globalThis.fetch + +// The browser, modelled: { cached: path -> stale text } in the HTTP cache, { wrong: path -> text } served instead of +// the real file, { missing: [path] } not served at all. +function world(host, { cached = {}, wrong = {}, missing = [] } = {}) { + const http = new Map(Object.entries(cached).map(([p, t]) => [`${host}/${p}`, t])) + const requests = [] + globalThis.fetch = async (input, init = {}) => { + const url = String(input) + if (!url.startsWith(host + '/')) return realFetch(input, init) + const path = url.slice(host.length + 1).split('?')[0] + const mode = init.cache || 'default' + requests.push({ path, mode }) + let text = null + if (mode !== 'reload' && mode !== 'no-store' && http.has(url)) text = http.get(url) + else { + text = missing.includes(path) ? null : (path in wrong ? wrong[path] : served(path)) + if (text !== null) http.set(url, text) + } + return text === null ? new Response('not found', { status: 404 }) : new Response(text, { status: 200 }) + } + return { requests } +} +const stale = (p) => `/* an older build */\n${served(p)}` + +async function auditFetchPinned(shaSrc) { + const fails = [] + const { fetchPinned, sha384 } = await asModule(shaSrc) + if (typeof fetchPinned !== 'function') return ['A: library/packet/sha384.js exports no fetchPinned'] + const P = 'lib/packet-strings.json', current = served(P), pin = await sha384(current) + // A + { const w = world('https://a.test', { cached: { [P]: stale(P) } }); let got = null, err = null + try { got = await fetchPinned(`https://a.test/${P}`, pin) } catch (e) { err = e } + if (got !== current) fails.push(`A: a stale cached copy did not give the current file (${err ? `rejected: ${err.message}` : 'returned other bytes'})`) + else if (w.requests.length !== 2 || w.requests[0].mode !== 'force-cache' || w.requests[1].mode !== 'reload') fails.push(`A: expected force-cache then ONE reload, got ${JSON.stringify(w.requests)}`) } + // B + { const w = world('https://b.test', { cached: { [P]: current } }); let got = null + try { got = await fetchPinned(`https://b.test/${P}`, pin) } catch { /* reported below */ } + if (got !== current || w.requests.length !== 1 || w.requests[0].mode !== 'force-cache') fails.push(`B: a matching cached copy should be used with ONE force-cache request, got ${JSON.stringify(w.requests)}`) } + // C + { const bad = current.replace('"en"', '"en" ') + ' ' + const w = world('https://c.test', { cached: { [P]: stale(P) }, wrong: { [P]: bad } }); let got = null, err = null + try { got = await fetchPinned(`https://c.test/${P}`, pin) } catch (e) { err = e } + if (got !== null || !err || err.integrity !== true) fails.push(`C: wrong bytes on the server were not refused as an integrity failure (${got !== null ? 'ACCEPTED' : err?.message})`) + else if (w.requests.length !== 2) fails.push(`C: expected exactly one fresh refetch before refusing, got ${w.requests.length} request(s)`) } + // D + { world('https://d.test', { missing: [P] }); let err = null + try { await fetchPinned(`https://d.test/${P}`, pin) } catch (e) { err = e } + if (!err || err.integrity) fails.push(`D: an unserved file should reject as not served (${err ? err.message : 'it resolved'})`) } + globalThis.fetch = realFetch + return fails +} + +async function auditLoaders() { + const fails = [] + // E: the packet text + { world('https://e.test', { cached: { 'lib/packet-strings.json': stale('lib/packet-strings.json') } }) + const { loadPacketStrings } = await import(join(REPO, 'library/packet/packetStrings.js')) + const warn = console.warn; console.warn = () => {} + const t = await loadPacketStrings('https://e.test').finally(() => { console.warn = warn }) + const src = JSON.parse(served('lib/packet-strings.json')) + if (Object.keys(t).join() === 'en' || t.zh?.zone_affinity_A017 !== src.zh?.zone_affinity_A017) fails.push(`E: a stale cached packet-strings.json made the packet English (got languages: ${Object.keys(t).join(',')})`) } + // F: India's states + { world('https://f.test', { cached: { 'people-groups/india-states.json': stale('people-groups/india-states.json') } }) + const { loadIndiaStates } = await import(join(REPO, 'library/data/indiaStates.js')) + let n = -1, err = null + try { n = (await loadIndiaStates('https://f.test')).states.length } catch (e) { err = e } + const want = JSON.parse(served('people-groups/india-states.json')).states.length + if (n !== want) fails.push(`F: a stale cached india-states.json lost India's states (${err ? `rejected: ${err.message}` : `${n} of ${want}`})`) } + // G: page 1's generator + { world('https://g.test', { cached: { 'lib/poster-render.js': stale('lib/poster-render.js'), 'boundaries/ne-places.json': stale('boundaries/ne-places.json') } }) + const { generateSheet, countryZone } = await import(join(REPO, 'library/packet/posterSheets.js')) + const zone = countryZone({ id: 'country-tcd', iso3: 'TCD', label: 'Chad', count: 1, region: 'Africa' }) + const pins = [[12.1, 15.0], [13.8, 20.8], [9.1, 18.4]].map(([lat, lng], i) => ({ name: `Probe ${i + 1}`, lat, lng, slug: `probe-${i}`, country_code: 'TCD', population: 1000 })) + const adm0 = JSON.parse(served('boundaries/world-adm0.geojson')) + let html = '', err = null + const warn = console.warn; console.warn = () => {} + try { html = await generateSheet({ zone, pins, format: '3x3', seriesId: 'country', adm0, assetsBase: 'https://g.test', date: '2026-09-29' }) } catch (e) { err = e } + console.warn = warn + if (!(typeof html === 'string' && / readFileSync(join(REPO, f), 'utf8') +if (process.argv.includes('--self-test')) { + const FOR = "for (const cache of ['force-cache', 'reload']) {" + const poisons = [ + ['fetchPinned without its fresh refetch', () => auditFetchPinned(shaSrc.replace(FOR, "for (const cache of ['force-cache']) {")), /^A: /], + ['fetchPinned that never uses the cache', () => auditFetchPinned(shaSrc.replace(FOR, "for (const cache of ['reload', 'reload']) {")), /^B: /], + ['fetchPinned that accepts a mismatch', () => auditFetchPinned(shaSrc.replace(' if (got === pin) return text\n', ' return text\n')), /^C: /], + ['a loader that fetches its pinned file directly', () => auditWiring((f) => (f === 'library/packet/packetStrings.js' + ? read(f).replace('fetchPinned(url, PACKET_STRINGS_SHA384)', "(await fetch(url, { cache: 'force-cache' })).text()") : read(f))), /^H: library\/packet\/packetStrings\.js/], + ] + let missed = 0 + for (const [name, run, rx] of poisons) { + const f = await run(); const hit = f.find((x) => rx.test(x)) + console.log(hit ? `✓ caught: ${name} -> ${hit}` : `✗ MISSED: ${name}${f.length ? ` (failed instead: ${f[0]})` : ''}`) + if (!hit) missed++ + } + if (missed) process.exitCode = 1 +} else { + const f = [...await auditFetchPinned(shaSrc), ...await auditLoaders(), ...auditWiring(read)] + if (f.length) { for (const x of f) console.log('✗ ' + x); console.log(`FAIL check:pinned-fetch-fresh (${f.length})`); process.exitCode = 1 } + else console.log('PASS check:pinned-fetch-fresh — a stale cached copy of a pinned file is refetched once, fresh: page 1 still draws, the packet keeps its language, India keeps its states; a matching cached copy is used as is; wrong bytes are refused; every pinned loader goes through fetchPinned') +} diff --git a/library/data/indiaStates.js b/library/data/indiaStates.js index 78cf38693..a0d510a1e 100644 --- a/library/data/indiaStates.js +++ b/library/data/indiaStates.js @@ -15,7 +15,7 @@ // t.byShapeName → Map(shapeName → entry) (the map's ADM1 features carry shapeName) // t.byIso → Map(iso → entry) import { INDIA_STATES_SHA384 } from './indiaStates.integrity.js' -import { sha384 } from '../packet/sha384.js' +import { fetchPinned } from '../packet/sha384.js' const PAGE_BASE = globalThis.document?.baseURI || undefined const cache = new Map() @@ -30,10 +30,11 @@ export function loadIndiaStates(assetsBase) { try { url = indiaStatesUrl(assetsBase) } catch (e) { return Promise.reject(e) } if (!cache.has(url)) { const p = (async () => { - const res = await fetch(url, { cache: 'force-cache' }) - if (!res.ok) throw new Error(`india-states.json not served (${res.status})`) - const text = await res.text() - if ((await sha384(text)) !== INDIA_STATES_SHA384) throw new Error('india-states.json failed its integrity check') + // force-cache, and once more fresh on a mismatch: a stale cached copy after a re-pin must not cost a returning + // reader India's state cards (../packet/sha384.js fetchPinned says why). + let text + try { text = await fetchPinned(url, INDIA_STATES_SHA384) } + catch (e) { throw new Error(`india-states.json ${e?.message || e}`) } const data = JSON.parse(text) const states = Array.isArray(data.states) ? data.states : [] return { diff --git a/library/packet/packetStrings.js b/library/packet/packetStrings.js index a9a50be3f..4284af4cb 100644 --- a/library/packet/packetStrings.js +++ b/library/packet/packetStrings.js @@ -18,7 +18,7 @@ */ import { en as EN } from '../scripts/posters/packet-strings.json' import { PACKET_STRINGS_SHA384 } from './poster-render.integrity.js' -import { sha384 } from './sha384.js' +import { fetchPinned } from './sha384.js' // Captured when the module evaluates, as in posterSheets.js: a relative asset base resolves against the page // that loaded the map, even if a single-page host changes its URL before the click. @@ -33,11 +33,9 @@ export function loadPacketStrings(assetsBase) { try { url = new URL(`${assetsBase}/lib/packet-strings.json`, PAGE_BASE).href } catch { return Promise.resolve(ENGLISH_ONLY) } if (!cache.has(url)) { const p = (async () => { - const res = await fetch(url, { cache: 'force-cache' }) - if (!res.ok) throw new Error(`not served (${res.status})`) - const text = await res.text() - if ((await sha384(text)) !== PACKET_STRINGS_SHA384) throw new Error('failed its integrity check') - return JSON.parse(text) + // force-cache, and once more fresh on a mismatch: a stale cached copy after a re-pin must not turn a + // returning reader's packet English (sha384.js fetchPinned says why). + return JSON.parse(await fetchPinned(url, PACKET_STRINGS_SHA384)) })().catch((e) => { console.warn(`[packetStrings] ${url}: ${e?.message || e} — printing in English`) cache.delete(url) // a later print tries again rather than inheriting one failure diff --git a/library/packet/posterSheets.js b/library/packet/posterSheets.js index 3f6282d0c..ad01a9e42 100644 --- a/library/packet/posterSheets.js +++ b/library/packet/posterSheets.js @@ -16,7 +16,7 @@ * ISO3_NAME, series: { totalGroups, totalLists, index } }) */ import { POSTER_RENDER_SHA384, NE_PLACES_SHA384, POSTER_RENDER_GLOBAL_SHA384 } from './poster-render.integrity.js' -import { sha384 } from './sha384.js' +import { fetchPinned } from './sha384.js' // ★ THE GENERATOR IS SERVED, NOT BUNDLED, AND IT IS INTEGRITY-PINNED. poster-render.js is served verbatim // at /lib/poster-render.js (generate-tree) and loaded at the FIRST print: a single-file build @@ -33,9 +33,11 @@ const PAGE_BASE = globalThis.document?.baseURI || undefined const abs = (u) => new URL(u, PAGE_BASE).href // The classic-script path: the browser checks POSTER_RENDER_GLOBAL_SHA384 itself and refuses to run a // file that does not match (onerror), so nothing unverified executes here either. +// A refused tag is tried ONCE more after a fetch with cache: 'reload' has refreshed the HTTP cache: a stale cached +// copy after a re-pin fails the browser's own check, exactly as it fails fetchPinned's (sha384.js says why). function loadClassic(url) { if (globalThis.DoxaPosterRender?.createPosterGenerator) return Promise.resolve(globalThis.DoxaPosterRender.createPosterGenerator) - return new Promise((resolve, reject) => { + const tag = () => new Promise((resolve, reject) => { const el = document.createElement('script') el.src = url el.integrity = POSTER_RENDER_GLOBAL_SHA384 @@ -44,9 +46,13 @@ function loadClassic(url) { el.onload = () => (globalThis.DoxaPosterRender?.createPosterGenerator ? resolve(globalThis.DoxaPosterRender.createPosterGenerator) : reject(new Error(`the sheet generator at ${url} loaded but did not define DoxaPosterRender`))) - el.onerror = () => reject(new Error(`the sheet generator at ${url} was refused (integrity or network) — not running it`)) + el.onerror = () => { el.remove(); reject(new Error(`the sheet generator at ${url} was refused (integrity or network) — not running it`)) } document.head.appendChild(el) }) + return tag().catch(async (first) => { + try { await fetch(url, { cache: 'reload' }) } catch { throw first } + return tag() + }) } const generatorCache = new Map() function loadGenerator(assetsBase) { @@ -54,13 +60,10 @@ function loadGenerator(assetsBase) { const url = abs(`${assetsBase}/lib/poster-render.js`) if (!generatorCache.has(url)) { const p = (async () => { - const res = await fetch(url, { cache: 'force-cache' }) - if (!res.ok) throw new Error(`the sheet generator is not served (${res.status}) at ${url}`) - const text = await res.text() - const got = await sha384(text) - if (got !== POSTER_RENDER_SHA384) { - throw new Error(`the sheet generator at ${url} failed its integrity check (expected ${POSTER_RENDER_SHA384}, got ${got}) — refusing to run it`) - } + // force-cache, and once more fresh on a mismatch (sha384.js fetchPinned says why); only a match runs. + let text + try { text = await fetchPinned(url, POSTER_RENDER_SHA384) } + catch (e) { throw new Error(`the sheet generator at ${url} ${e?.message || e} — refusing to run it`) } const blobUrl = URL.createObjectURL(new Blob([text], { type: 'text/javascript' })) try { return (await import(/* @vite-ignore */ blobUrl)).createPosterGenerator } catch (e) { @@ -96,11 +99,10 @@ function loadPlaces(assetsBase) { const url = abs(`${assetsBase}/boundaries/ne-places.json`) if (!placesCache.has(url)) { const p = (async () => { - const res = await fetch(url, { cache: 'force-cache' }) - if (!res.ok) return [] - const text = await res.text() - if ((await sha384(text)) !== NE_PLACES_SHA384) { - console.warn(`[posterSheets] ${url} failed its integrity check — drawing no city dots`) + let text + try { text = await fetchPinned(url, NE_PLACES_SHA384) } + catch (e) { + if (e?.integrity) console.warn(`[posterSheets] ${url} failed its integrity check, also when fetched fresh — drawing no city dots`) return [] } return JSON.parse(text).places || [] diff --git a/library/packet/sha384.js b/library/packet/sha384.js index 3890ca2bb..2d63cfc5b 100644 --- a/library/packet/sha384.js +++ b/library/packet/sha384.js @@ -7,3 +7,25 @@ export async function sha384(text) { for (const b of d) bin += String.fromCharCode(b) return `sha384-${btoa(bin)}` } + +// ★ A PINNED FILE THAT FAILS ITS PIN IS FETCHED ONCE MORE, FRESH. The loaders fetch with cache: 'force-cache' (fast, +// and it works offline once cached), which returns ANY cached copy, however old. After a re-pin, a reader who had +// printed before kept the previous file: it failed its pin on every print, and the print fell back without a word +// (the stored poster for page 1, English for the packet text) until the browser evicted it. So a copy that is not +// served or does not match is fetched ONCE more with cache: 'reload', which skips the HTTP cache and refreshes it, +// and only that answer decides. Resolves with the verified text; rejects with the last reason (err.integrity is +// true when the bytes were wrong rather than missing). +export async function fetchPinned(url, pin) { + let last = null + for (const cache of ['force-cache', 'reload']) { + try { + const res = await fetch(url, { cache }) + if (!res.ok) { last = new Error(`not served (${res.status})`); continue } + const text = await res.text() + const got = await sha384(text) + if (got === pin) return text + last = Object.assign(new Error(`failed its integrity check (expected ${pin}, got ${got})`), { integrity: true }) + } catch (e) { last = e } + } + throw last +} diff --git a/package.json b/package.json index a9750bab0..c7069314b 100644 --- a/package.json +++ b/package.json @@ -356,8 +356,8 @@ "dev": "bun x vite", "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 check:preview-flag-dev-only:self-test check:poster-type-registry:self-test check:host-floor:self-test check:phone-print-keep: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:pins-in-state check:fork-leaders check:circular-layout 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-sections check:poster-sections:self-test check:poster-design-catalogue check:poster-design-catalogue:self-test 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 check:preview-flag-dev-only check:poster-type-registry check:host-floor check:phone-print-keep", + "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 check:preview-flag-dev-only:self-test check:poster-type-registry:self-test check:host-floor:self-test check:phone-print-keep:self-test check:pinned-fetch-fresh: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:pins-in-state check:fork-leaders check:circular-layout 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-sections check:poster-sections:self-test check:poster-design-catalogue check:poster-design-catalogue:self-test 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 check:preview-flag-dev-only check:poster-type-registry check:host-floor check:phone-print-keep check:pinned-fetch-fresh", "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", @@ -436,9 +436,12 @@ "check:host-floor": "node internal/check-host-floor.mjs", "check:host-floor:self-test": "node internal/check-host-floor.mjs --self-test", "check:phone-print-keep": "node internal/check-phone-print-keep.mjs", - "check:phone-print-keep:self-test": "node internal/check-phone-print-keep.mjs --self-test" + "check:phone-print-keep:self-test": "node internal/check-phone-print-keep.mjs --self-test", + "check:pinned-fetch-fresh": "bun internal/check-pinned-fetch-fresh.mjs", + "check:pinned-fetch-fresh:self-test": "bun internal/check-pinned-fetch-fresh.mjs --self-test" }, "//scripts": { + "check:pinned-fetch-fresh": "a STALE cached copy of a pinned print-time file never decides a print: library/packet/sha384.js fetchPinned fetches force-cache, and on a copy that is not served or fails its pin, ONCE more with cache:'reload'; every pinned loader (posterSheets poster-render.js + ne-places.json, packetStrings, indiaStates) calls it, and the classic-script path refreshes the cache and retries a refused tag once. With a modelled HTTP cache holding stale copies: the current file after one fresh refetch; a matching cached copy used with one force-cache request; wrong bytes refused; page 1 still draws (generateSheet), the packet keeps every language, India keeps its 36 states. Self-test poisons: no fresh refetch, never the cache, a mismatch accepted, a loader fetching its pinned file directly.", "check:phone-print-keep": "on a PHONE the in-page print document (#dxm-print-root and its print style, library/composables/printInPlace.js) and printImageBlob's object URL outlive afterprint, because Android Chrome fires afterprint once the PREVIEW is built and renders a saved PDF later, from the page as it is then (a phone's saved packet was the bare host page). A timeline on fake timers and a stub DOM with an Android user agent: still mounted 30 s after afterprint; released once the reader is back (shown again after hidden, focused again after blur, or touched after print() but not in its first TOUCH_SETTLE_MS) plus BACK_GRACE_MS (60 s), or at the ceiling; going away again during the grace cancels the pending drop; the Print tap of a late print does not count; a new print replaces it; desktop is the frame path, verbatim. --impl= runs it against another copy (the pre-fix file fails). Self-test poisons: the old drop at afterprint, no release when back, listeners armed at mount, no grace, printImageBlob revoking at afterprint, no ceiling, no cancel when away again, no touch settle window.", "_": "What each script does. These notes used to sit inside the command strings as trailing \"# …\" comments, which made the shell discard any arguments passed after \"--\" (so a gate given --url silently measured its hardcoded default port — possibly another agent's server). Keep notes here, never in a command.", "browser-gates": "lists every package.json script that launches a browser (the same detection run-parallel uses to route them through internal/browser-slot.sh). Run each as internal/browser-slot.sh bun run .",