New gate check:tree-relative: the committed tree names no host of its own
An absolute embed address works where it was built and breaks everywhere else, which is how the CDN route failed and was retired. The build config keeps embedBaseUrl empty, so the committed tree is relative and a deploy makes it absolute at build time (EMBED_BASE_URL). But nothing stopped a tree built with EMBED_BASE_URL from being committed: check:drop looks for root-relative "/..." references and deliberately skips https:// ones. With embedBaseUrl empty in the config, the manifest must say embedBase "." and every scriptUrl, iframeUrl and embed snippet src must be relative. Data addresses (runtime.apiBaseUrl) are not the tree and are not checked. SKIPs when the config sets embedBaseUrl. In gates:post, after check:drop. Today's tree: PASS, embedBase "." and 173 embed addresses, all relative. Poison-tested, each fails: an absolute snippet src, an absolute embedBase, a protocol-relative iframeUrl, an empty manifest. A real deploy manifest built with EMBED_BASE_URL fails with 174 findings, as it should. gates:post: PASS 82, NOT MEASURED 13 (browser/server), FAIL 1: the known check:packets-all worktree failure (no generated zone packets here).
This commit is contained in:
parent
8d8b38a4b2
commit
93ebe64984
2 changed files with 75 additions and 1 deletions
72
internal/check-tree-relative.mjs
Normal file
72
internal/check-tree-relative.mjs
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
#!/usr/bin/env node
|
||||
// check-tree-relative — the committed tree names no host of its own.
|
||||
//
|
||||
// ★ WHY. An absolute embed address works where it was built and breaks everywhere else. That
|
||||
// is how the CDN route failed and was retired (2026-09-29): its absolute addresses worked on
|
||||
// a laptop and broke in the cloud. The build config's embedBaseUrl is "" on purpose, so the
|
||||
// committed tree is relative and a deploy makes it absolute at build time (EMBED_BASE_URL,
|
||||
// read by generate-tree). But a tree built with EMBED_BASE_URL set looks like any other tree,
|
||||
// and check:drop cannot see the difference: it looks for root-relative "/..." references and
|
||||
// deliberately skips https:// ones. Committing such a tree would pin every snippet to one host.
|
||||
//
|
||||
// With embedBaseUrl empty in the build config, the manifest must say embedBase "." and every
|
||||
// address a page or a partner loads the tree from must be relative: each bundle's and profile's
|
||||
// scriptUrl and iframeUrl, and the src of each copy-paste embed snippet. Data addresses
|
||||
// (runtime.apiBaseUrl, instance props) are not the tree and are not checked. With embedBaseUrl
|
||||
// set in the config, absolute addresses are intended: SKIP.
|
||||
//
|
||||
// Usage: node internal/check-tree-relative.mjs [manifest.json] (default: the built tree's)
|
||||
// Static: reads files, starts nothing. Exit 0 pass or skip, 1 fail.
|
||||
import { existsSync, readFileSync } from 'node:fs'
|
||||
import { dirname, join, resolve } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { TREE_NAME, BUILD_CONFIG_FILE } from './_tree-name.mjs'
|
||||
|
||||
const REPO = resolve(dirname(fileURLToPath(import.meta.url)), '..')
|
||||
const cfg = JSON.parse(readFileSync(BUILD_CONFIG_FILE, 'utf8'))
|
||||
const configured = typeof cfg.embedBaseUrl === 'string' ? cfg.embedBaseUrl.trim() : ''
|
||||
if (configured) {
|
||||
console.log(`TREE-RELATIVE: SKIP — the build config sets embedBaseUrl (${configured}), so absolute embed addresses are intended`)
|
||||
process.exit(0)
|
||||
}
|
||||
|
||||
const file = process.argv[2] ? resolve(process.argv[2]) : join(REPO, 'public', TREE_NAME, 'manifest.json')
|
||||
if (!existsSync(file)) {
|
||||
console.error(`TREE-RELATIVE: FAIL — no manifest at ${file} (build the tree first)`)
|
||||
process.exit(1)
|
||||
}
|
||||
const manifest = JSON.parse(readFileSync(file, 'utf8'))
|
||||
|
||||
const absolute = (u) => /^[a-z][a-z0-9+.-]*:/i.test(u) || u.startsWith('//')
|
||||
const srcs = (html) => [...String(html || '').matchAll(/\bsrc="([^"]*)"/g)].map((m) => m[1])
|
||||
const fails = []
|
||||
let checked = 0
|
||||
const check = (where, url) => {
|
||||
if (typeof url !== 'string' || !url) return
|
||||
checked += 1
|
||||
if (absolute(url)) fails.push(`${where}: ${url}`)
|
||||
}
|
||||
|
||||
if (manifest.embedBase !== '.' && manifest.embedBase !== '') fails.push(`embedBase: ${JSON.stringify(manifest.embedBase)}`)
|
||||
for (const b of manifest.bundles || []) {
|
||||
check(`${b.bundle} scriptUrl`, b.scriptUrl)
|
||||
for (const p of b.profiles || []) {
|
||||
const at = `${b.bundle}/${p.profile}`
|
||||
check(`${at} scriptUrl`, p.scriptUrl)
|
||||
check(`${at} iframeUrl`, p.iframeUrl)
|
||||
for (const s of srcs(p.embed?.script)) check(`${at} script snippet src`, s)
|
||||
for (const s of srcs(p.embed?.iframe)) check(`${at} iframe snippet src`, s)
|
||||
}
|
||||
}
|
||||
|
||||
// Refuse to pass vacuously: a manifest with no addresses means the walk or the shape is wrong.
|
||||
if (checked === 0) fails.push('no embed addresses found in the manifest: the check read nothing')
|
||||
|
||||
if (fails.length) {
|
||||
console.error(`TREE-RELATIVE: FAIL — ${fails.length} problem(s) in ${file}`)
|
||||
for (const f of fails.slice(0, 20)) console.error(` ✗ ${f}`)
|
||||
if (fails.length > 20) console.error(` … and ${fails.length - 20} more`)
|
||||
console.error(' A tree built with EMBED_BASE_URL is for a deploy, not a commit: rebuild it without EMBED_BASE_URL.')
|
||||
process.exit(1)
|
||||
}
|
||||
console.log(`TREE-RELATIVE: PASS — embedBase "${manifest.embedBase}" and ${checked} embed addresses, all relative`)
|
||||
|
|
@ -304,6 +304,7 @@
|
|||
"check:targets:self-test": "node internal/check-targets.mjs --self-test",
|
||||
"check:tauri": "node internal/check-tauri-config.mjs",
|
||||
"check:template-literal": "node internal/template-literal-check.mjs",
|
||||
"check:tree-relative": "node internal/check-tree-relative.mjs",
|
||||
"check:tree-weight": "node internal/check-published-tree-weight.mjs",
|
||||
"check:upg-map-data": "node internal/check-upg-map-data-painted.mjs",
|
||||
"check:url-resolver": "node internal/check-url-resolver.mjs",
|
||||
|
|
@ -320,7 +321,7 @@
|
|||
"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:done-evidence check:script-flags check:backlog-status 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:backlog-refs 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: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: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:poster-print-fallback check:posters-exportable check:country-names check:reduced-motion check:no-dead-library check:universal-props check:readme-size",
|
||||
"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:done-evidence check:script-flags check:backlog-status 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:backlog-refs 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: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: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: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",
|
||||
"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: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:gospel-whiteboard check:packet-map-missing-loud check:poster-layout-optin check:ported-globals-identical check:ring-bands check:packet-a1-structure 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-new-driver-references check:poster-fetch-guard check:people-group-records",
|
||||
"gen:iso3-to-alpha2": "node library/scripts/generate-iso3-to-alpha2.mjs",
|
||||
"internet:adm1": "node plugin-designs/cities-internet/scripts/build-adm1-internet.mjs",
|
||||
|
|
@ -499,6 +500,7 @@
|
|||
"check:packet-strings-parity": "the PACKET catalog (packet-strings.json) must be whole in every language: all string keys, all 12 prayer points with every field, all 12 wheel rhythms. check:i18n-parity covers library/i18n/locales only — six gates READ this file and none checked it. Asserts PRESENCE, not translatedness, with point4_passage declared-absent on record.",
|
||||
"check:packet-variants": "the poster wall's Prayer List prints the PACKET (not the columnar list), and every variant it offers exists in packet-render's VARIANTS. A variant may drop fixed pages but never people: lightweight keeps every group. By the UPG-100 owner; mutation-tested. Headless; in gates:pre.",
|
||||
"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: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.",
|
||||
"check:universal-props": "every plugin reads the props the Plugin Studio header sets for all of them through the library: map engine, locale, Mapbox token and token URL, and the people-group list. It reads each bundle's source AND its shipped bundle and fails on a bypass (a mapbox:// style literal, a private token global, a list fetched around the library); a by-design exception is ALLOWed by name with its reason. --report also prints the matrix. Headless.",
|
||||
"check:zone-title-tier-a": "generated zone titles ship for ONE-country zones only: the 3 such zones produce a localized, non-code title in en/es/ar that differs across languages, AND every other zone declines so its hand-written label survives unchanged (a multi-country zone has a name that is not a list of its members). Two-sided, and NOT MEASURED when the country-name supplier cannot resolve a known code.",
|
||||
"check:evangelical-share": "the packet's % evangelical page states THIS list's share: summed over every DOXA group the per-group table reproduces christian-share.json exactly; different lists give different figures; a list with no published levels prints a dash, never 0% (Number(null) is 0); the coverage clause appears only when partial.",
|
||||
|
|
|
|||
Loading…
Reference in a new issue