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:
parent
04c2a3d2eb
commit
2a9198fa3c
2 changed files with 208 additions and 63 deletions
|
|
@ -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.')
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
Loading…
Reference in a new issue