From 4970059e55dae67dd065b7da32586c2ee6337050 Mon Sep 17 00:00:00 2001 From: doxa-maps-88655c70 <88627970+doxa-maps-88655c70@users.noreply.github.com> Date: Sun, 20 Sep 2026 09:05:18 +0000 Subject: [PATCH] add a label-join gate: fail on any new equality predicate comparing two independently-derived values --- internal/check-label-joins.mjs | 77 ++++++++++++++++++++++++++++++++++ package.json | 3 +- 2 files changed, 79 insertions(+), 1 deletion(-) create mode 100644 internal/check-label-joins.mjs diff --git a/internal/check-label-joins.mjs b/internal/check-label-joins.mjs new file mode 100644 index 000000000..19c4252ba --- /dev/null +++ b/internal/check-label-joins.mjs @@ -0,0 +1,77 @@ +#!/usr/bin/env node +/** + * check-label-joins.mjs — assert no NEW label-keyed equality predicate enters the codebase. + * + * ★ WHY THIS EXISTS — a join is only as safe as the number of INDEPENDENT DERIVATIONS feeding it. + * A Mapbox filter of the shape ['==', ['get', 'Label'], value] compares a feature property + * against a value produced SOMEWHERE ELSE. If the two sides are derived by different fallback + * chains, the predicate silently matches ZERO features — a blank map, with no error anywhere. + * + * TAXONOMY (measured across ~80 equality predicates in this repo, 2026-09-20): + * SAFE-1 compared against a LITERAL ('Available', true, '__none__') — no second derivation. + * SAFE-2 compared against a CODE/SLUG both sides read from the SAME field (affinityBlock, + * uniqueId, countryIso, doxaRegion) — ONE derivation, used twice. + * DANGER compared against a LABEL derived by a DIFFERENT chain than wrote the property. + * Only the DANGER class needs guarding, and it is ENUMERABLE IN ONE GREP — which is the property + * that makes this checkable by someone who does not know the domain, and re-checkable after any + * change. (Framing is the weak half of a rule; the grep is the tool.) + * + * ★ THE FIX PATTERN when one appears: do NOT make the chains agree — that leaves N derivations and + * adds a constraint nothing enforces, which is the condition that produces the bug. CARRY THE + * VALUE THAT BUILT THE PROPERTY (see `originalLabels` on the dialect path: built by both producers, + * threaded through mapStore + GeocoderComponent to the filter, verified end-to-end). That removes + * N-1 derivations, converting DANGER into SAFE-2 — true by construction, not by discipline. + * PREFER FIXES THAT REMOVE A DERIVATION OVER FIXES THAT ADD A RULE. + * + * KNOWN/ACCEPTED: the two occurrences in useGeocoderSearch.js are a fallback branch that is never + * reached (the code branch wins whenever blocCode/clusterCode is present, and useSearch.js attaches + * them to every such event from fields the source contract DOES map). They are dead rather than + * broken. Allow-listed with that reasoning so this gate starts GREEN and any NEW one fails loudly. + * + * Usage: node internal/check-label-joins.mjs (exit 0 clean / 1 new danger found) + */ +import { readdirSync, readFileSync } from 'node:fs' +import { join, resolve, dirname, relative } from 'node:path' +import { fileURLToPath } from 'node:url' + +const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..') +const SCAN = ['library', 'plugin-designs'] +// ['==', ['get', 'somethingLabel'], …] — a label-keyed equality predicate. +const DANGER = /\[\s*'=='\s*,\s*\[\s*'get'\s*,\s*'([a-zA-Z_]*[Ll]abel)'\s*\]/g + +// Accepted occurrences: ":" — each MUST carry a reason in the header above. +const ALLOW = new Set([ + 'library/composables/useGeocoderSearch.js:affinityBlockLabel', + 'library/composables/useGeocoderSearch.js:clusterLabel', +]) + +const hits = [] +const scan = (dir) => { + let entries + try { entries = readdirSync(dir, { withFileTypes: true }) } catch { return } + for (const e of entries) { + if (e.name.startsWith('.') || e.name === 'node_modules') continue + const p = join(dir, e.name) + if (e.isDirectory()) { scan(p); continue } + if (!/\.(js|vue|mjs)$/.test(e.name)) continue + const rel = relative(ROOT, p) + readFileSync(p, 'utf8').split('\n').forEach((line, i) => { + DANGER.lastIndex = 0 + let m + while ((m = DANGER.exec(line))) hits.push({ rel, line: i + 1, prop: m[1] }) + }) + } +} +for (const d of SCAN) scan(join(ROOT, d)) + +const unexpected = hits.filter((h) => !ALLOW.has(`${h.rel}:${h.prop}`)) +console.log(`LABEL-JOINS: scanned ${SCAN.join(' + ')} — ${hits.length} label-keyed equality predicate(s), ${ALLOW.size} allow-listed.`) +if (unexpected.length) { + console.error(`LABEL-JOINS: FAIL — ${unexpected.length} NEW label-keyed equality predicate(s):`) + for (const h of unexpected) console.error(` ${h.rel}:${h.line} ['==', ['get', '${h.prop}'], …]`) + console.error(" A feature property compared against a value derived ELSEWHERE matches ZERO features when the") + console.error(" two derivations diverge — silently. Either compare a CODE both sides read from one field, or") + console.error(" CARRY the value that built the property (see originalLabels on the dialect path).") + process.exit(1) +} +console.log('LABEL-JOINS: PASS — no new two-derivation label join.') diff --git a/package.json b/package.json index 6c554ccd1..b0f4816e4 100644 --- a/package.json +++ b/package.json @@ -42,7 +42,8 @@ "sync:dropin": "node internal/sync-dropin.mjs", "deploy:dev": "bun run build && node internal/sync-dropin.mjs --from-config --prune # destination + allowlist come from vite.doxa-maps-build-config.json (dropin.dest / dropin.include); DOXA_DROPIN_DEST= overrides the destination", "check:public-hygiene": "node internal/public-hygiene-check.mjs", - "build:unlocked": "bun run check:template-literal && 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 gallery", + "check:label-joins": "node internal/check-label-joins.mjs", + "build:unlocked": "bun run check:template-literal && 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 gallery", "check:template-literal": "node internal/template-literal-check.mjs", "verify:map-paint": "node internal/verify-map-paint.mjs", "check:sri": "node internal/sri-consistency-check.mjs",