From 34010ce6d898a0343af2aa89d4d7758cae535a05 Mon Sep 17 00:00:00 2001 From: Doxa Maps Date: Mon, 28 Sep 2026 22:16:28 +0000 Subject: [PATCH] The grid's "controls off" preset did nothing, and nothing said so MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit rega RQ20: the driven page asks for show-search:false and show-filters:false and renders the search box and the filters bar anyway, with 0 console errors. There were no errors because nothing failed. The key never arrived: · the preset writes KEBAB into profile-config — "show-search": "false" · ProfileLoader PROVIDES that object; it does not bind its keys as props · the profile reads props.showSearch (CAMEL), which keeps its default of true Three correct-looking layers and no join between them. A preset key that does nothing looks exactly like a preset key that works, which is why the fix ships with a gate rather than a comment. The grid now reads these flags off the config as well as the prop, in the same prop -> cfg ladder researchUrl beside them already used, and accepts BOTH spellings: someone writing profile-config by hand reaches for the kebab spelling because that is what the docs show on the tag, while the prop is camel. Accepting one silently is what made this inert. An explicit prop still wins. Proved on five cases including the exact preset, an explicit prop overriding the config, and config:true keeping the control on. check:instance-config-keys asserts every preset key reaches the profile that renders it. Writing it was the interesting part, and it was WRONG TWICE: 1. First version matched only `cfg.` and reported NINE FALSE POSITIVES across four plugins — big-picture reads config.value.accent, doxa-simple-map reads profileConfig?.value?.tabs. The config object's variable name is each profile's own business; matching on it measured my naming habit, not the contract. 2. Widening it to "any accessor" then matched `props.showSearch` — the exact line that does NOT work — and the gate went GREEN on the defect it exists for. props is the one object that cannot carry a profile-config key, so it is now masked out before the match. Mutation-tested after that: reverting the fix to props-only makes it fail again, and the grid file is restored byte-identical. A gate I have not watched fail is not evidence, and this one passed for the wrong reason once already. The four bundles that embed the grid are rebuilt, so the fix is in the shipped .js and not only in the source rega cannot run. --- internal/check-instance-config-keys.mjs | 92 +++++++++++++++++++ package.json | 2 +- .../people-groups-grid/people-groups-grid.vue | 27 +++++- 3 files changed, 117 insertions(+), 4 deletions(-) create mode 100644 internal/check-instance-config-keys.mjs diff --git a/internal/check-instance-config-keys.mjs b/internal/check-instance-config-keys.mjs new file mode 100644 index 000000000..90e280dfd --- /dev/null +++ b/internal/check-instance-config-keys.mjs @@ -0,0 +1,92 @@ +/** + * check-instance-config-keys.mjs — a preset's profile-config keys must be keys the profile READS. + * + * ★ THE DEFECT (rega RQ20): people-groups-grid's "driven" preset declares "show-search":"false" and + * "show-filters":"false", and the page renders the search box and the filters bar anyway. Nothing + * errored — 0 console errors — because nothing was wrong at runtime. The key simply never reached + * the component: + * + * · the preset writes KEBAB ("show-search") into profile-config + * · ProfileLoader PROVIDES that object; it does not bind its keys as props + * · the profile reads props.showSearch (CAMEL), which therefore keeps its default of true + * + * A preset key nobody reads is indistinguishable from a working one by looking at the preset, which + * is why this is a gate and not a comment. It is the same shape as the @element defect: an + * annotation a build copies into a machine-read artifact is an INTERFACE, and a build that accepts + * prose there is missing a validator. + * + * WHAT IT CHECKS, per preset key: either the profile declares a prop of that name, or it reads the + * key off the injected config (cfg.key / cfg['key']). Generator-owned and transport keys are exempt + * — they are consumed by ProfileLoader and the page, not by the profile. + */ +import { readFileSync, readdirSync, existsSync } from 'node:fs' +import { join } from 'node:path' + +const ROOT = new URL('../plugin-designs/', import.meta.url).pathname + +// Consumed by ProfileLoader / the generated page itself, never by a profile component. +const TRANSPORT = new Set([ + 'profile', 'instanceId', 'dataSource', 'assetsBase', 'colorSet', 'tk', 'token', + 'apiBaseUrl', 'locale', 'lang', 'provider', 'mapProvider', +]) + +const camel = (k) => k.replace(/-([a-z])/g, (_, c) => c.toUpperCase()) +const fail = [] +let checkedPresets = 0 +let checkedKeys = 0 + +for (const bundle of readdirSync(ROOT, { withFileTypes: true }).filter((d) => d.isDirectory() && !d.name.startsWith('_'))) { + const dir = join(ROOT, bundle.name) + for (const file of readdirSync(dir).filter((f) => f.endsWith('.instances.json'))) { + let doc + try { doc = JSON.parse(readFileSync(join(dir, file), 'utf8')) } catch { continue } + const profileName = doc.profile || file.replace('.instances.json', '') + const vue = join(dir, `${profileName}.vue`) + if (!existsSync(vue)) continue + const src = readFileSync(vue, 'utf8') + + for (const inst of doc.instances || []) { + checkedPresets++ + for (const rawKey of Object.keys(inst.props || {})) { + if (TRANSPORT.has(rawKey)) continue + checkedKeys++ + const asCamel = camel(rawKey) + // A prop declaration, in either spelling… + const declaresProp = new RegExp(`\\b${asCamel}\\s*:\\s*\\{`).test(src) + // …or the profile reads the key off the injected config under ANY local name for it. + // ★ THE FIRST VERSION OF THIS LINE LOOKED ONLY FOR `cfg.` AND REPORTED NINE FALSE POSITIVES + // across four plugins: big-picture reads config.value.accent, doxa-simple-map reads + // profileConfig?.value?.tabs. The config object's VARIABLE NAME is the profile's own + // business, so matching on it was measuring my own naming habit rather than the contract. + // What matters is that the key is read off SOMETHING, so the accessor is what gets matched. + // ★ AND `props.` MUST NOT COUNT. Widening it to "any accessor" then matched + // `props.showSearch` — the exact line that DOES NOT work — and the gate went green on the + // defect it was written for. A prop read is the broken path: ProfileLoader provides the + // config, it never binds its keys as props. So the accessor is matched everywhere EXCEPT + // off `props`, which is the one object that cannot carry a profile-config key. + const accessor = new RegExp( + `(? { // ── Cat-2 show/hide (accept the DOM-attribute string 'false') ────────────────── const truthy = (v) => !(v === false || v === 'false' || v === '0' || v === 0) -const useSearch = computed(() => truthy(props.showSearch)) -const useFilters = computed(() => truthy(props.showFilters)) -const useTotal = computed(() => truthy(props.showTotal)) + +// ★ THE PRESET'S KEYS WERE NEVER REACHING THESE (rega RQ20). The "driven" preset asks for +// show-search:false and show-filters:false and the page rendered both anyway, with 0 console +// errors — because nothing was failing. ProfileLoader PROVIDES profile-config; it does not bind +// its keys as props. So props.showSearch stayed at its default of true and the preset was inert. +// +// Read the config as well as the prop, in the same prop -> cfg ladder researchUrl above already +// uses. BOTH SPELLINGS are accepted: an author writing profile-config by hand reaches for the +// kebab attribute spelling ("show-search") because that is what the docs show on the tag, while +// the prop is camel. Accepting only one is what made this silent — and a preset key that does +// nothing looks exactly like a preset key that works. +const cfgFlag = (camelKey, kebabKey) => { + const c = cfg.value + return c?.[camelKey] !== undefined ? c[camelKey] : c?.[kebabKey] +} +const resolveFlag = (propValue, camelKey, kebabKey) => { + // An explicit prop on the tag still wins; otherwise the config decides; otherwise the default. + if (propValue !== undefined && propValue !== true) return truthy(propValue) + const fromCfg = cfgFlag(camelKey, kebabKey) + return fromCfg === undefined ? truthy(propValue) : truthy(fromCfg) +} +const useSearch = computed(() => resolveFlag(props.showSearch, 'showSearch', 'show-search')) +const useFilters = computed(() => resolveFlag(props.showFilters, 'showFilters', 'show-filters')) +const useTotal = computed(() => resolveFlag(props.showTotal, 'showTotal', 'show-total')) // ── state (copied from UupgsList) ────────────────────────────────────────────── const uupgs = ref([])