The grid's "controls off" preset did nothing, and nothing said so
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.
This commit is contained in:
parent
51cd0e3628
commit
34010ce6d8
3 changed files with 117 additions and 4 deletions
92
internal/check-instance-config-keys.mjs
Normal file
92
internal/check-instance-config-keys.mjs
Normal file
|
|
@ -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(
|
||||
`(?<!\\bprops)(?:\\.value)?\\??\\.${asCamel}\\b|\\[['\"]${rawKey}['\"]\\]|\\[['\"]${asCamel}['\"]\\]`
|
||||
)
|
||||
// A key may also be read INDIRECTLY — resolved through a helper that is handed both
|
||||
// spellings as arguments, which is how the RQ20 fix reads them. A literal occurrence of the
|
||||
// kebab key anywhere OUTSIDE a props access counts: it cannot get into the file by accident.
|
||||
const masked = src.replace(/\bprops\.(\w+)/g, 'PROPSREAD_$1')
|
||||
const readsFromCfg = accessor.test(masked) || masked.includes(`'${rawKey}'`) || masked.includes(`"${rawKey}"`)
|
||||
if (!declaresProp && !readsFromCfg) {
|
||||
fail.push(`${bundle.name}/${inst.id}: "${rawKey}" — ${profileName}.vue declares no ${asCamel} prop and never reads it off the config`)
|
||||
} else if (rawKey !== asCamel && declaresProp && !readsFromCfg) {
|
||||
// THE RQ20 CASE: the prop exists in camel, the preset writes kebab, and nothing bridges
|
||||
// them — ProfileLoader provides the config, it does not bind keys as props.
|
||||
fail.push(`${bundle.name}/${inst.id}: "${rawKey}" is KEBAB but ${profileName}.vue reads props.${asCamel}; profile-config keys are not bound as props, so this preset key does nothing`)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (fail.length) {
|
||||
console.error('INSTANCE-CONFIG-KEYS: FAIL — a preset declares a key its profile never reads.')
|
||||
for (const f of fail) console.error(` ✗ ${f}`)
|
||||
process.exit(1)
|
||||
}
|
||||
console.log(`INSTANCE-CONFIG-KEYS: PASS — ${checkedKeys} key(s) across ${checkedPresets} preset(s) reach the profile that renders them.`)
|
||||
|
|
@ -287,7 +287,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:capability-derived 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:zoning-declared 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 check:no-cross-plugin-imports check:no-cross-plugin-imports:self-test check:map-attribution check:studio-rail check:approx-pin-note check:embedder-keys check:provider-for check:legend-logical-insets check:legend-logical-insets:self-test check:optional-t-equivalence check:text-highlight check:religion-label-not-value check:i18n-stopped-scope check:no-silent-engine check:regions-uses-library check:shadow-css-parses check:square-map-controls check:preset-config-merge check:frozen-labels",
|
||||
"gates:post": "node internal/run-parallel.mjs --label=post -- check:capability-derived 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:zoning-declared 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 check:no-cross-plugin-imports check:no-cross-plugin-imports:self-test check:map-attribution check:studio-rail check:approx-pin-note check:embedder-keys check:provider-for check:legend-logical-insets check:legend-logical-insets:self-test check:optional-t-equivalence check:text-highlight check:religion-label-not-value check:i18n-stopped-scope check:no-silent-engine check:regions-uses-library check:shadow-css-parses check:square-map-controls check:preset-config-merge check:instance-config-keys check:frozen-labels",
|
||||
"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 check:setup-order check:package-json-unique-keys check:no-repo-pb-hooks check:no-credentials-omit check:no-credentials-omit:self-test check:shadow-reduced-motion check:poster-action-print check:targets check:targets:self-test check:browser-gates-guarded check:pin-selection check:facet-shape check:hardcoded-english:self-test check:list-selection check:selector-fields check:upg-zone-joins check:gospel-set-charts check:describe-group check:one-scroll-container check:packet-charts check:composite-token-url check:language-tree-layout check:i18n-keys-exist check:packet-variants check:profile-table check:element-bootstrap check:embed-url-absolute check:frame-target check:language-tree-model check:map-menu-anchor check:selection-kinds check:tiered-points check:whiteboard-projection check:mercator-slice check:ring-vocabulary check:map-props-unit check:mobile-gating check:provide-shadowing check:legend-top-offset",
|
||||
"gen:iso3-to-alpha2": "node library/scripts/generate-iso3-to-alpha2.mjs",
|
||||
"internet:adm1": "node plugin-designs/cities-internet/scripts/build-adm1-internet.mjs",
|
||||
|
|
|
|||
|
|
@ -388,9 +388,30 @@ const t = computed(() => {
|
|||
|
||||
// ── 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([])
|
||||
|
|
|
|||
Loading…
Reference in a new issue