check:one-palette reads only the PALETTE export's values, not every hex to the end of the file
Some checks are pending
Deploy to GitHub Pages / build (push) Waiting to run
Deploy to GitHub Pages / deploy (push) Blocked by required conditions

It claimed to read "only the PALETTE export" but matched every hex from `export const PALETTE` to the
END OF THE FILE, comments included. A later note such as "REJECTED #AF7AC5, scored 2.95, under the 3.0
floor" (regions' affinity-block.js, being prepared) would have obliged every dashboard to ship a colour
rejected on WCAG grounds. paletteHexes() now slices to the export's matching closing brace and reads
hexes only inside string literals. --self-test (check:one-palette:self-test, gates:post) plants a hex
in an inline comment, a block comment, a trailing comment and a later export, and requires only the
two real values back. The verdict on main is unchanged (PASS, 6 palettes).
This commit is contained in:
Doxa Maps 2026-09-26 08:03:23 +00:00
parent 20c29442e8
commit 06ea24c98a
2 changed files with 52 additions and 7 deletions

View file

@ -63,17 +63,61 @@ if (!existsSync(BUILT)) {
process.exit(0)
}
/**
* The hexes of the PALETTE export ONLY: from `export const PALETTE` to its matching closing brace, and
* only inside string literals. It used to match every hex from the export to the END OF THE FILE,
* comments included, so a later note such as "REJECTED #AF7AC5, scored 2.95, under the 3.0 floor"
* (affinity-block.js) obliged every consumer to ship a colour rejected on WCAG grounds.
* Returns null when the module has no PALETTE export.
*/
export function paletteHexes(src) {
const at = src.indexOf('export const PALETTE')
if (at < 0) return null
const open = src.indexOf('{', at), openArr = src.indexOf('[', at)
const start = [open, openArr].filter((i) => i >= 0).sort((a, b) => a - b)[0]
if (start === undefined) return []
const close = { '{': '}', '[': ']' }[src[start]]
let depth = 0, i = start, quote = null
const strings = []
let cur = ''
for (; i < src.length; i++) {
const c = src[i], n = src[i + 1]
if (quote) {
if (c === '\\') { cur += c + n; i++; continue }
if (c === quote) { strings.push(cur); cur = ''; quote = null; continue }
cur += c; continue
}
if (c === '/' && n === '/') { i = src.indexOf('\n', i); if (i < 0) break; continue }
if (c === '/' && n === '*') { i = src.indexOf('*/', i + 2) + 1; if (i <= 0) break; continue }
if (c === "'" || c === '"' || c === '`') { quote = c; continue }
if (c === src[start]) depth++
else if (c === close && --depth === 0) break
}
return [...new Set(strings.flatMap((t) => [...t.matchAll(/#[0-9a-fA-F]{6}\b/g)].map((m) => m[0].toLowerCase())))]
}
if (process.argv.includes('--self-test')) {
const src = [
"export const PALETTE = {",
" a: '#112233', // not #445566 (a comment inside the export)",
" /* nor #778899 */ b: \"#aabbcc\",",
"}",
"// REJECTED #af7ac5, scored 2.95, under the 3.0 floor",
"export const OTHER = { c: '#ddeeff' }",
].join('\n')
const got = paletteHexes(src).join(',')
if (got !== '#112233,#aabbcc') { console.error(`ONE-PALETTE: SELF-TEST FAIL — read ${got}, want #112233,#aabbcc`); process.exit(1) }
console.log('ONE-PALETTE: SELF-TEST PASS — only the PALETTE export\'s string values are read; comments and later code are not.')
process.exit(0)
}
// ── the expected palettes, read from the strategy modules the MAP uses ────────────────────────
const strategies = {}
for (const f of readdirSync(COLORS)) {
if (!f.endsWith('.js') || f.startsWith('_') || f.startsWith('.')) continue
const src = readFileSync(join(COLORS, f), 'utf8')
// Only the PALETTE export — a module may also contain incidental hexes (a default, a comment)
// that no consumer is obliged to ship.
const at = src.indexOf('export const PALETTE')
if (at < 0) continue
const hexes = [...new Set([...src.slice(at).matchAll(/#[0-9a-fA-F]{6}/g)].map((m) => m[0].toLowerCase()))]
if (hexes.length) strategies[f.replace(/\.js$/, '')] = hexes
const hexes = paletteHexes(src)
if (hexes && hexes.length) strategies[f.replace(/\.js$/, '')] = hexes
}
// ── the consumers that must agree ─────────────────────────────────────────────────────────────

View file

@ -132,6 +132,7 @@
"check:no-orphan-gates": "node internal/check-no-orphan-gates.mjs",
"check:no-public-binds": "node internal/check-no-public-binds.mjs",
"check:one-palette": "node internal/check-one-palette.mjs",
"check:one-palette:self-test": "node internal/check-one-palette.mjs --self-test",
"check:packet-browser": "node internal/check-packet-browser.mjs",
"check:packet-fixture": "node internal/generate-packet-fixture.mjs --check",
"check:packet-render": "node internal/check-packet-render.mjs",
@ -199,7 +200,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: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: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",
"gates:post": "node internal/run-parallel.mjs --label=post -- 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: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",
"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",
"library-index": "node internal/generate-library-index.mjs",
"lists": "node library/scripts/people-groups/build-lists.mjs",