Assert the shipped heartbeat ramp, not just the panel's fallback

check:heartbeat proved the PANEL FALLBACK was intact and called that "default
ramp unchanged". It is not the same property. The running app resolves the ramp
with dashboard.config.js's map.* layered on top of that fallback, and the gate
never read the real config: assertion F checked only that heartbeatThreshold was
DECLARED, never what it was SET TO.

Measured consequence, reproduced before fixing:

    sed -i 's/heartbeatThreshold: 150,/heartbeatThreshold: 999,/' \
      plugin-designs/research-dashboard/dashboard.config.js
    node internal/check-heartbeat-threshold.mjs   # exit 0

The shipped map could silently change its ramp while the gate reported it
unchanged. Now exits 1, naming the key and its value.

Adds assertion A2: resolve the ramp the way the running app does — panel
defaults with the shipped map.* applied — and require it to equal the historical
5/20/60/150/Infinity with its labels. A (fallback) and A2 (shipped) share one
comparison helper so they cannot drift. A2 also rejects a shipped heartbeatMode
other than 'fixed': a data-derived ramp is right for an unknown dataset and
wrong for this one, where 150 is deliberate. Changing the shipped ramp is now a
deliberate act that must update HISTORICAL in the same commit.

dashboard.config.js is read by brace-matching its `map:` literal rather than
import(): it pulls in @map/* aliases and a JSON module, so importing it would
need Vite's resolver — a dependency that buys nothing for a bag of scalars.

The self-test now runs three cases, each required to fire the assertion that
exists for it, and poisons in memory so a crash cannot leave the tree edited:

    [literal]         resolver replaced by the old array        -> must fire B
    [shipped-default] shipped heartbeatThreshold changed        -> must fire A2
    [shipped-mode]    shipped heartbeatMode flipped to quantile -> must fire A2

The header previously claimed the gate was "negative-tested" on the strength of
the first case alone. True of that case, misleading overall, so it now states
per-case coverage and what is NOT covered (colour contrast, and whether the map
paints at all). Testing that the FEATURE works — a configured threshold moves
the edge — is not testing that the REGRESSION is caught, and only the second
protects the requirement that today's map must not change.
This commit is contained in:
doxa-maps-88655c70 2026-09-22 22:44:10 +00:00
parent 04c2a3d2eb
commit 2a9198fa3c
2 changed files with 208 additions and 63 deletions

View file

@ -25,34 +25,58 @@
* today's rendering. And it false-NEGATIVES: a re-hard-coding that used 200, or that hard-coded
* only the three lower edges, would sail straight through. Substring presence is not the property.
*
* The property is: DOES CHANGING THE CONFIG CHANGE THE MAP? So this gate EXECUTES the resolver
* extracted from the live SFC and asserts the bins actually MOVE when the config moves. A literal
* cannot pass that, whatever numbers it uses.
* The property is TWO-SIDED, and getting only one side is how this gate was wrong once already:
* 1. DOES CHANGING THE CONFIG CHANGE THE MAP? (configurability — assertion B)
* 2. DOES THE SHIPPED CONFIG STILL RENDER TODAY'S MAP? (no regression — assertion A2)
* So this gate EXECUTES the resolver extracted from the live SFC, against BOTH synthetic configs
* and the REAL dashboard.config.js. A literal fails (1); an edited shipped default fails (2).
*
* ASSERTIONS
* A. DEFAULT IS UNCHANGED — with NO config, the bin assignment over the committed fixture is
* EXACTLY the historical ramp (edges 5/20/60/150/Infinity, labels included). This is the
* non-negotiable half: configurability must not have cost us today's map.
* B. THE THRESHOLD IS LIVE — setting heartbeatThreshold to 40 and to 500 must MOVE the top edge
* to exactly that number and must RE-BIN the fixture's countries. This is the assertion a
* re-hard-coded literal fails.
* C. THE LEGEND FOLLOWS THE EDGES — the top label must read "<threshold>+". A moved edge with a
* stale '150+' legend is confidently wrong, which is worse than no legend.
* D. NO DEAD BINS — every bin must be reachable (`colorFor` tests `count < upTo`, so an edge of
* 1 creates a band no count can enter: the ramp paints 4 of its 5 colours while the legend
* advertises 5). Found live during this card at threshold 40, and again in quantile mode.
* E. EDGES ARE STRICTLY ASCENDING — a non-monotonic ramp lets an earlier bin shadow a later one.
* F. THE CONFIG DECLARES IT — dashboard.config.js must carry map.heartbeatThreshold, so the
* knob has a documented home rather than living only as a defaulted inject.
* A. THE PANEL'S FALLBACK IS INTACT — with NO config at all, the resolver yields EXACTLY the
* historical ramp (edges 5/20/60/150/Infinity, labels included).
* A2. THE SHIPPED CONFIGURATION RENDERS THAT SAME RAMP — resolved the way the RUNNING APP does,
* with dashboard.config.js's map.* layered on top of the panel defaults. ★ A2 IS THE
* ASSERTION THAT PROTECTS "TODAY'S MAP MUST NOT CHANGE", AND IT WAS MISSING AT FIRST: A
* alone proves only the FALLBACK, which is strictly weaker, and F below proves only that the
* key is DECLARED, not what it is SET TO. Measured consequence — editing the shipped config
* to heartbeatThreshold:999 changed the real map while this gate printed "default ramp
* unchanged" and exited 0. Changing the shipped ramp is now a deliberate act that must also
* update HISTORICAL below, in the same commit.
* B. THE THRESHOLD IS LIVE — setting heartbeatThreshold to 40 and to 500 must MOVE the top edge
* to exactly that number and must RE-BIN the fixture's countries. This is the assertion a
* re-hard-coded literal fails.
* C. THE LEGEND FOLLOWS THE EDGES — the top label must CONTAIN "<threshold>+" and name the
* heartbeat zone. A moved edge with a stale '150+' legend is confidently wrong, which is
* worse than no legend. Matched by property, not equality — see the note on HISTORICAL.
* D. NO DEAD BINS — every bin must be reachable (`colorFor` tests `count < upTo`, so an edge of
* 1 creates a band no count can enter: the ramp paints 4 of its 5 colours while the legend
* advertises 5). Found live during this card at threshold 40, and again in quantile mode.
* E. EDGES ARE STRICTLY ASCENDING — a non-monotonic ramp lets an earlier bin shadow a later one.
* F. THE CONFIG DECLARES IT — dashboard.config.js must carry map.heartbeatThreshold. PRESENCE
* ONLY; the VALUE is A2's job. These are separate on purpose, because conflating them is
* exactly how the 999 regression shipped past this gate.
*
* ── DATA ────────────────────────────────────────────────────────────────────────────────────
* internal/fixtures/facet-one-field-rows.json — 114 REAL rows (59 countries). Not synthetic: this
* repo deleted a synthetic fallback precisely because it produced confident wrong answers.
*
* ── NEGATIVE TEST ───────────────────────────────────────────────────────────────────────────
* `bun run check:heartbeat:self-test` re-hard-codes the ramp IN MEMORY (the exact regression: the
* resolver replaced by the old literal array) and requires THIS gate to REJECT it. A gate that
* cannot fail is worse than no gate — this repo has shipped one.
* ── NEGATIVE TESTS, AND EXACTLY WHAT THEY COVER ─────────────────────────────────────────────
* `bun run check:heartbeat:self-test` runs THREE cases, each requiring the assertion that exists
* for it to fire. Stated precisely so the next reader does not over-trust this gate:
*
* [literal] the resolver is replaced by the old hard-coded array → must fire B
* [shipped-default] dashboard.config.js's heartbeatThreshold is changed → must fire A2
* [shipped-mode] dashboard.config.js flips heartbeatMode to 'quantile' → must fire A2
*
* ★ THE FIRST VERSION OF THIS GATE SHIPPED WITH [literal] ONLY, and its header said it had been
* "negative-tested" — true of that case, misleading overall. Testing that THE FEATURE WORKS (a
* configured threshold moves the edge) is NOT testing that THE REGRESSION IS CAUGHT (the default
* silently changing), and only the second protects the Driver. One negative test per regression
* claimed. A gate that cannot fail is worse than no gate — this repo has shipped one.
*
* NOT COVERED, deliberately: this gate reasons about BIN ASSIGNMENT and LABELS only. It says
* nothing about the colours themselves (contrast against the pin is argued and measured in
* DashboardMapPanel.vue) and nothing about whether the map actually PAINTS (see verify:map-paint).
*/
import { readFileSync } from 'node:fs'
import { fileURLToPath } from 'node:url'
@ -105,6 +129,40 @@ function makeResolver(mapCfg, srcOverride) {
return fn({ map: mapCfg })
}
// ── the SHIPPED map config ──────────────────────────────────────────────────────────────────
// Read as SOURCE TEXT, not `import()`. dashboard.config.js imports '@map/…' aliases and a JSON
// module, so importing it from a bare node gate would need Vite's resolver — a dependency that
// would make this gate fragile for no benefit. The `map:` literal is a self-contained bag of
// scalars, so brace-matching and evaluating just that block reads the real shipped values with
// nothing else to resolve.
// Set by the self-test to simulate an edited dashboard.config.js WITHOUT touching the file — a
// self-test that mutated the repo could leave the tree poisoned if it crashed mid-run.
let SHIPPED_CONFIG_OVERRIDE = null
function shippedMapConfig() {
if (SHIPPED_CONFIG_OVERRIDE) return SHIPPED_CONFIG_OVERRIDE
const src = readFileSync(CONFIG, 'utf8')
const at = src.search(/^\s{2}map:\s*\{/m)
if (at < 0) {
fail('A2. Could not find the top-level `map: {` block in dashboard.config.js.\n' +
' This gate must read the SHIPPED map config; if the block moved, update this gate.')
return null
}
let depth = 0
const start = src.indexOf('{', at)
let i = start
for (; i < src.length; i++) {
if (src[i] === '{') depth++
else if (src[i] === '}') { depth--; if (!depth) { i++; break } }
}
try {
// eslint-disable-next-line no-eval
return eval('(' + src.slice(start, i) + ')')
} catch (e) {
fail(`A2. The \`map\` block in dashboard.config.js did not evaluate: ${e.message}`)
return null
}
}
// ── the fixture, binned ─────────────────────────────────────────────────────────────────────
function countryCounts() {
const rows = JSON.parse(readFileSync(FIXTURE, 'utf8'))
@ -134,22 +192,55 @@ function run(srcOverride) {
const vals = [...counts.values()]
const resolve0 = (cfg) => makeResolver(cfg, srcOverride)(vals)
// A. DEFAULT UNCHANGED
const def = resolve0({})
HISTORICAL.forEach((exp, i) => {
const got = def[i]
if (!got || got.upTo !== exp.upTo) {
fail(`A. DEFAULT RAMP MOVED at bin ${i}: expected upTo=${exp.upTo}, got ${got ? got.upTo : '(missing)'}.\n` +
' With no config supplied the dashboard must render EXACTLY as it always has.')
} else if (exp.l === null) {
if (!topLabelOk(got.l, 150)) {
fail(`A. DEFAULT TOP LABEL is "${got.l}" — it must contain "150+" and name the heartbeat zone.`)
// Compare a resolved ramp against the historical one. Shared by A and A2 so the two can never
// drift apart — the whole defect A2 fixes was one path being asserted and the other not.
const expectHistorical = (bins, tag, why) => {
HISTORICAL.forEach((exp, i) => {
const got = bins[i]
if (!got || got.upTo !== exp.upTo) {
fail(`${tag}. RAMP MOVED at bin ${i}: expected upTo=${exp.upTo}, got ${got ? got.upTo : '(missing)'}.\n ${why}`)
} else if (exp.l === null) {
if (!topLabelOk(got.l, 150)) {
fail(`${tag}. TOP LABEL is "${got.l}" — it must contain "150+" and name the heartbeat zone.\n ${why}`)
}
} else if (got.l !== exp.l) {
fail(`${tag}. LEGEND LABEL MOVED at bin ${i}: expected "${exp.l}", got "${got.l}".\n ${why}`)
}
})
if (bins.length !== HISTORICAL.length) fail(`${tag}. RAMP has ${bins.length} bins, expected ${HISTORICAL.length}.\n ${why}`)
}
// A. THE PANEL'S FALLBACK IS INTACT — with NO config supplied at all.
const def = resolve0({})
expectHistorical(def, 'A', 'With no config supplied the panel must fall back to EXACTLY the historical ramp.')
// ── A2. THE SHIPPED CONFIGURATION RENDERS THE HISTORICAL RAMP ───────────────────────────────
// ★ THIS IS THE ASSERTION THAT PROTECTS THE DRIVER, AND IT WAS MISSING. A alone proves the
// PANEL'S FALLBACK is intact — a strictly weaker property than "the map still looks the same",
// because the running app resolves with dashboard.config.js's map.* layered ON TOP of that
// fallback. Assertion F only checked that heartbeatThreshold was DECLARED, never what it was
// SET TO. So editing the shipped config to 999 changed the real map while this gate printed
// "default ramp unchanged" and exited 0. Measured, reproduced, and the reason A2 exists.
//
// A2 resolves the ramp THE WAY THE RUNNING APP DOES and requires it to equal the historical one.
// Changing the shipped default is now a deliberate act that must also update HISTORICAL here.
if (srcOverride === undefined) {
const shipped = shippedMapConfig()
if (shipped) {
expectHistorical(
makeResolver(shipped, srcOverride)(vals), 'A2',
'This is the ramp dashboard.config.js ACTUALLY SHIPS (map.heartbeatThreshold=' +
`${shipped.heartbeatThreshold}, binThresholds=${JSON.stringify(shipped.binThresholds)}, ` +
`mode=${shipped.heartbeatMode}). Today\'s map must not change. If this move is intended, ` +
'update HISTORICAL in this gate in the same commit.',
)
if (shipped.heartbeatMode && shipped.heartbeatMode !== 'fixed') {
fail(`A2. dashboard.config.js ships heartbeatMode='${shipped.heartbeatMode}'.\n` +
" 'fixed' is the shipped default on purpose: a data-derived ramp changes per dataset,\n" +
' which is right for an unknown dataset and wrong for this one, where 150 is deliberate.')
}
} else if (got.l !== exp.l) {
fail(`A. DEFAULT LEGEND LABEL MOVED at bin ${i}: expected "${exp.l}", got "${got.l}".`)
}
})
if (def.length !== HISTORICAL.length) fail(`A. DEFAULT RAMP has ${def.length} bins, expected ${HISTORICAL.length}.`)
}
const baseline = assignment(def, counts)
@ -183,7 +274,10 @@ function run(srcOverride) {
})
}
// F. THE CONFIG DECLARES IT
// F. THE CONFIG DECLARES IT.
// NOTE this is a PRESENCE check only, and presence is NOT value — A2 is what asserts the value.
// Keeping them separate (and saying so) is deliberate: F alone once let heartbeatThreshold:999
// ship while this gate printed "default ramp unchanged".
const cfgSrc = readFileSync(CONFIG, 'utf8')
if (!/heartbeatThreshold\s*:/.test(cfgSrc)) {
fail('F. dashboard.config.js does not declare map.heartbeatThreshold.\n' +
@ -192,32 +286,82 @@ function run(srcOverride) {
return errors.slice()
}
// ── NEGATIVE TEST ───────────────────────────────────────────────────────────────────────────
// Re-hard-code the ramp exactly as it was before this card, and require the gate to REJECT it.
// ── NEGATIVE TESTS ──────────────────────────────────────────────────────────────────────────
// ★ TWO CASES, BECAUSE THERE ARE TWO DISTINCT REGRESSIONS AND THEY ARE NOT THE SAME ASSERTION.
// This gate originally shipped with case 1 only, and its header claimed it was "negative-tested".
// That was true of case 1 and MISLEADING overall: case 2 — the one that actually protects the
// Driver's "today's map must not change" requirement — was unasserted, and a one-line edit to
// dashboard.config.js changed the shipped ramp while the gate printed PASS. Verified by
// reproducing it, not by reasoning about it.
//
// The general shape, worth more than either case: testing that THE FEATURE WORKS (a configured
// threshold moves the edge) is not testing that THE REGRESSION IS CAUGHT (the default silently
// changing). A gate needs a negative test per regression it claims to prevent.
const CASES = [
{
id: 'literal',
what: 'the resolver is replaced by a re-hard-coded literal ramp (configurability removed)',
mustFire: 'B',
apply: () => ({
srcOverride: `
const BIN_COLORS = ['#f8e0da', '#fccec4', '#febbad', '#ffa797', '#ff9380']
function resolveBins() {
return [
{ upTo: 5, c: BIN_COLORS[0], l: '1–4' }, { upTo: 20, c: BIN_COLORS[1], l: '5–19' },
{ upTo: 60, c: BIN_COLORS[2], l: '20–59' }, { upTo: 150, c: BIN_COLORS[3], l: '60–149' },
{ upTo: Infinity, c: BIN_COLORS[4], l: '150+' },
]
}
`,
}),
},
{
id: 'shipped-default',
what: "dashboard.config.js's shipped map.heartbeatThreshold is changed (today's map moves)",
mustFire: 'A2',
// Simulates `sed -i 's/heartbeatThreshold: 150,/heartbeatThreshold: 999,/'` WITHOUT editing
// the file, so a crash mid-self-test cannot leave the repo poisoned.
apply: () => {
SHIPPED_CONFIG_OVERRIDE = { mapStyle: 'paper', heartbeatThreshold: 999, binThresholds: [5, 20, 60], heartbeatMode: 'fixed', heartbeatQuantile: 0.1 }
return {}
},
},
{
id: 'shipped-mode',
what: "dashboard.config.js flips heartbeatMode to 'quantile' (ramp becomes data-derived)",
mustFire: 'A2',
apply: () => {
SHIPPED_CONFIG_OVERRIDE = { mapStyle: 'paper', heartbeatThreshold: 150, binThresholds: [5, 20, 60], heartbeatMode: 'quantile', heartbeatQuantile: 0.1 }
return {}
},
},
]
if (SELF_TEST) {
const POISON = `
const BIN_COLORS = ['#f8e0da', '#fccec4', '#febbad', '#ffa797', '#ff9380']
function resolveBins() {
return [
{ upTo: 5, c: BIN_COLORS[0], l: '1–4' }, { upTo: 20, c: BIN_COLORS[1], l: '5–19' },
{ upTo: 60, c: BIN_COLORS[2], l: '20–59' }, { upTo: 150, c: BIN_COLORS[3], l: '60–149' },
{ upTo: Infinity, c: BIN_COLORS[4], l: '150+' },
]
let bad = 0
for (const c of CASES) {
SHIPPED_CONFIG_OVERRIDE = null
const { srcOverride } = c.apply()
const got = run(srcOverride)
SHIPPED_CONFIG_OVERRIDE = null
if (!got.length) {
console.error(`SELF-TEST FAILED [${c.id}]: the gate ACCEPTED ${c.what}.\n` +
' A gate that cannot fail is worse than no gate.')
bad++
continue
}
`
const got = run(POISON)
if (!got.length) {
console.error('SELF-TEST FAILED: the gate ACCEPTED a re-hard-coded ramp. This gate cannot fail, so it is worse than no gate.')
process.exit(1)
if (!got.some((e) => e.startsWith(c.mustFire + '.'))) {
console.error(`SELF-TEST FAILED [${c.id}]: rejected, but NOT via assertion ${c.mustFire}, which is the one\n` +
` that exists for this regression. Rejections seen:\n ` + got.map((e) => e.split('\n')[0]).join('\n '))
bad++
continue
}
console.log(` ✓ [${c.id}] REJECTED via ${c.mustFire} — ${c.what}`)
got.forEach((e) => console.log(' · ' + e.split('\n')[0]))
}
const sawB = got.some((e) => e.startsWith('B.'))
if (!sawB) {
console.error('SELF-TEST FAILED: the gate rejected the poison, but not via assertion B (the threshold-is-live check).\n' +
'Rejections seen:\n ' + got.join('\n '))
process.exit(1)
}
console.log(`check:heartbeat SELF-TEST PASS — a re-hard-coded ramp is REJECTED (${got.length} assertion(s) fired, including B).`)
got.forEach((e) => console.log(' · ' + e.split('\n')[0]))
if (bad) { console.error(`\ncheck:heartbeat SELF-TEST FAILED — ${bad} of ${CASES.length} case(s) not caught.`); process.exit(1) }
console.log(`\ncheck:heartbeat SELF-TEST PASS — all ${CASES.length} regression case(s) rejected by their own assertion.`)
process.exit(0)
}
@ -227,5 +371,6 @@ if (found.length) {
found.forEach((e) => console.error(' ✗ ' + e + '\n'))
process.exit(1)
}
console.log('check:heartbeat PASS — default ramp unchanged (5/20/60/150/∞ over 59 real countries); ' +
'heartbeatThreshold 40 and 500 both move the edge, relabel the legend, and re-bin the fixture; no dead or non-monotonic bins.')
console.log('check:heartbeat PASS — the SHIPPED dashboard.config.js resolves to the historical ramp ' +
'(5/20/60/150/∞ over 59 real countries) and so does the panel fallback; heartbeatThreshold 40 and 500 ' +
'both move the edge, relabel the legend, and re-bin the fixture; no dead or non-monotonic bins.')

View file

@ -52,8 +52,8 @@
"check:facet-one-field:self-test": "node internal/check-facet-one-field.mjs --self-test",
"check:facet-changes-map": "node internal/check-facet-changes-map.mjs # a facet selection must CHANGE THE DOTS — and the dots must SAY what they mean. Asserts (1) the rd-bubbles feature set changes by count AND identity, (2) every surviving dot satisfies the selected level, (3) while a prayer filter is active circle-color is a DATA-DRIVEN EXPRESSION reading peoplePraying, not a flat literal. Assertion 3 is the one that matters: the prayer filter ALREADY narrowed 2107->689 correctly while the Driver reported it broken, because every dot painted the same orange. Browser gate (needs `bun run dev` on 5173, or BASE_URL/PROBE_PATH at a served tree), so NOT in build:unlocked. run the `check:facet-changes-map:self-test` script for the NEGATIVE test.",
"check:facet-changes-map:self-test": "node internal/check-facet-changes-map.mjs --self-test # reverts the fix IN THE LIVE PAGE and requires the gate to FAIL. SELF_TEST_MODE=colour (default, isolates the colour assertion — the reported bug) | props | both | frozen (blocks the source update, isolating the set assertion). All four modes verified caught.",
"check:heartbeat": "node internal/check-heartbeat-threshold.mjs # the HEARTBEAT ZONE threshold (the choropleth's top bin) must come from config, not a literal. The old 5/20/60/150 array was a DATASET assumption (~2000 people groups) wearing the costume of a constant: on a 200-group dataset every country falls in the bottom bin and the map renders uniformly pale — correct code, no crash, no error, meaningless map, invisible to every other gate. EXECUTES the resolver from the live SFC and asserts the bins MOVE when the config moves, because a grep for '150' both false-positives (it is the legitimate default) and false-negatives (a re-hard-coding at 200 sails through). run check:heartbeat:self-test for the NEGATIVE test.",
"check:heartbeat:self-test": "node internal/check-heartbeat-threshold.mjs --self-test # re-hard-codes the ramp IN MEMORY and requires check:heartbeat to REJECT it (asserting specifically that assertion B, threshold-is-live, fires). A gate that cannot fail is worse than no gate.",
"check:heartbeat": "node internal/check-heartbeat-threshold.mjs # the HEARTBEAT ZONE threshold (the choropleth's top bin) must come from config AND the shipped config must still render today's ramp. Two sides: (B) a configured threshold must MOVE the edge — catches re-hard-coding; (A2) dashboard.config.js's own map.* must still resolve to 5/20/60/150/Infinity — catches the shipped default silently changing. A2 was MISSING at first and heartbeatThreshold:999 shipped past this gate printing 'default ramp unchanged'. A grep for '150' cannot do either job: it false-positives on the legitimate default and false-negatives on a re-hard-coding at 200. run check:heartbeat:self-test for the NEGATIVE tests (3 cases).",
"check:heartbeat:self-test": "node internal/check-heartbeat-threshold.mjs --self-test # THREE regression cases, each required to fire ITS OWN assertion: [literal] resolver replaced by the old hard-coded array -> B; [shipped-default] dashboard.config.js heartbeatThreshold changed -> A2; [shipped-mode] heartbeatMode flipped to quantile -> A2. Poisons IN MEMORY, never the file. Testing that the FEATURE works is not testing that the REGRESSION is caught — this gate shipped with only the first and missed the second.",
"build:unlocked": "bun run check:template-literal && bun run check:mjs-parse && bun run check:sfc-parse && bun run clean && bun run token && for d in plugin-designs/*/; do BUNDLE=$(basename \"$d\") bun x vite build || exit 1; done && bun run tree && bun run check:drop && bun run check:parse && bun run verify && bun run check:sri && bun run check:public-hygiene && bun run check:label-joins && bun run check:facet-cascade && bun run check:facet-one-field && bun run check:heartbeat && bun run check:kpi-unit && bun run check:composite-reg && bun run check:descriptions && bun run check:no-abs-paths && bun run check:no-codename && bun run check:dep-names && bun run check:composite-srcs && bun run check:embed-targets && bun run check:nested-token && bun run gallery",
"check:template-literal": "node internal/template-literal-check.mjs",
"verify:map-paint": "node internal/verify-map-paint.mjs",