check:composite-interop: a composite's links must exist on both ends, in source and in the built bundles
A composite is a composite because its children talk, and every link is a name
written in two files that nothing ties together. Rename it on one side and the
build stays green while the link silently disappears. The interop matrix also
found the other silent way a link dies: research-map.vue looked up its host with
getCurrentInstance() inside a watcher callback, where Vue returns null, so
map-filtered was never sent, and the only proof had used a synthetic event.
Headless, in about a second:
- A: each link's send pattern is in the sender and its listen pattern in the
receiver (17 links across 7 composites).
- B: the literal is in every built bundle that ships an end of the link.
- C: every capability-4 composite is mapped, or states why it has no links
(stack-demo).
- D: no getCurrentInstance() inside a plain function in plugin-designs/ or
library/. The three routed sites are printed as KNOWN, and an entry left
behind after its fix fails.
Negative-tested on 521f8b18, and each of these fails: a receiver renamed in
source, a built bundle out of step, an unmapped composite, a new late
getCurrentInstance (one-line function, arrow, multi-line), and a stale KNOWN
entry. check:no-orphan-gates, check:script-flags and check:quoted-scripts pass.
It is not wired into gates:post; that is the build owner's call.
This commit is contained in:
parent
521f8b18d6
commit
6ef0e4623b
2 changed files with 233 additions and 0 deletions
231
internal/check-composite-interop.mjs
Normal file
231
internal/check-composite-interop.mjs
Normal file
|
|
@ -0,0 +1,231 @@
|
|||
#!/usr/bin/env node
|
||||
/**
|
||||
* check-composite-interop.mjs — the links between a composite's children must exist on BOTH ends.
|
||||
*
|
||||
* WHY THIS EXISTS. A capability-4 composite is only a composite because its children talk: the grid
|
||||
* narrows the map, "Show on map" moves the map, a region click retitles the dashboard. Every one of
|
||||
* those is a NAME (an event, an attribute, a config key) written in two files that nothing ties
|
||||
* together. Rename it on one side and the build stays green, both children still render, and the
|
||||
* link is silently gone. The interop matrix (2026-09-26) also found the other silent way a link dies:
|
||||
* research-map.vue found its host with getCurrentInstance() INSIDE a watcher callback, where Vue
|
||||
* returns null, so `map-filtered` was never dispatched, and the only "proof" had fired a synthetic
|
||||
* event at the composite. Neither shape needs a browser to see.
|
||||
*
|
||||
* WHAT IT CHECKS, headless:
|
||||
* A. SOURCE: each link's send pattern is in the sender file and its listen pattern is in the
|
||||
* receiver file (a rename on either side fails, naming both files).
|
||||
* B. BUILT: each link's literal is in every bundle that ships an end of it (what the reader
|
||||
* actually gets; a stale bundle fails here even when the source is right).
|
||||
* C. COVERAGE: every composite (*.meta.json type nested-elements-mfe) has links in LINKS or an
|
||||
* entry in NO_LINKS with its reason. A new composite with neither fails as "unmapped".
|
||||
* D. LATE getCurrentInstance(): in plugin-designs/ and library/, a getCurrentInstance() call
|
||||
* inside a plain function (not setup, not a lifecycle hook, not a use* composable body) is a
|
||||
* host lookup that returns null whenever that function runs from a watcher or an event. Capture
|
||||
* the instance in setup instead. Sites already routed to an owner are listed in LATE_KNOWN and
|
||||
* printed, not failed; delete the entry in the owner's fix commit so the gate enforces it again.
|
||||
*
|
||||
* NOT A SUBSTITUTE FOR A BROWSER. Both ends existing is necessary, not sufficient: UPG-100's
|
||||
* select-zone listener exists and still loses the first click, because the lazy Map pane attaches it
|
||||
* too late (rega, 2026-09-26). The runtime half is check:gallery-embed's `linked` phase.
|
||||
*
|
||||
* NEGATIVE-TESTED (2026-09-26, main 521f8b18, PASS with 17 links and the 3 LATE_KNOWN sites), each FAILS:
|
||||
* a receiver renamed in source ('map-filtered' -> 'map-filter' in research-map-plus-grid.vue);
|
||||
* a built bundle out of step ("country-selected" renamed in regions-dashboard.js only);
|
||||
* stack-demo's NO_LINKS entry removed (unmapped composite);
|
||||
* a new getCurrentInstance() in a one-line function, a one-line arrow and a multi-line function;
|
||||
* the emitFilterState site fixed with its LATE_KNOWN entry left behind (stale entry).
|
||||
*/
|
||||
import { readFileSync, readdirSync, existsSync } from 'node:fs'
|
||||
import { resolve, dirname, join, relative } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { TREE_NAME } from './_tree-name.mjs'
|
||||
|
||||
const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..')
|
||||
const PD = 'plugin-designs'
|
||||
const esc = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
||||
// Common send/listen shapes. `n` is the event name.
|
||||
const EMIT = (n) => new RegExp(`emit\\(\\s*['"]${esc(n)}['"]`)
|
||||
const VON = (n) => new RegExp(`@${esc(n)}=`)
|
||||
const DOM_SEND = (n) => new RegExp(`new CustomEvent\\(\\s*['"]${esc(n)}['"]|['"]${esc(n)}['"][\\s\\S]{0,400}dispatchEvent|dispatch\\(\\s*['"]${esc(n)}['"]`)
|
||||
const DOM_LISTEN = (n) => new RegExp(`addEventListener\\??\\.?\\(\\s*['"]${esc(n)}['"]`)
|
||||
const LIT = (s) => new RegExp(esc(s))
|
||||
const camelOn = (n) => 'on' + n.replace(/(^|-)([a-z])/g, (_, __, c) => c.toUpperCase())
|
||||
|
||||
// ── the links (PRODUCTION-MATRIX.md "Interop matrix") ─────────────────────────────────────────
|
||||
// send/recv: [file, RegExp]. built: [bundle, literal] — every bundle that ships an end of the link.
|
||||
const LINKS = [
|
||||
// big-picture: children compiled in (Vue emits + props)
|
||||
{ composite: 'big-picture', id: 'header facets -> grid (research-filter)',
|
||||
send: [`${PD}/big-picture/src/components/BpResearchHeader.vue`, EMIT('research-filter')],
|
||||
recv: [`${PD}/big-picture/big-picture.vue`, VON('research-filter')],
|
||||
built: [['big-picture', '"research-filter"'], ['big-picture', camelOn('research-filter')]] },
|
||||
{ composite: 'big-picture', id: 'grid total -> header count (groups-filtered)',
|
||||
send: [`${PD}/people-groups-grid/people-groups-grid.vue`, EMIT('groups-filtered')],
|
||||
recv: [`${PD}/big-picture/big-picture.vue`, VON('groups-filtered')],
|
||||
built: [['big-picture', '"groups-filtered"'], ['big-picture', camelOn('groups-filtered')]] },
|
||||
// dashboard-plus-grid: children compiled in
|
||||
{ composite: 'dashboard-plus-grid', id: 'dashboard facets -> grid (dashboard-filter)',
|
||||
send: [`${PD}/research-dashboard/research-dashboard.vue`, EMIT('dashboard-filter')],
|
||||
recv: [`${PD}/dashboard-plus-grid/dashboard-plus-grid.vue`, VON('dashboard-filter')],
|
||||
built: [['dashboard-plus-grid', '"dashboard-filter"'], ['dashboard-plus-grid', camelOn('dashboard-filter')]] },
|
||||
// research-map-plus-grid: children composed by src (DOM events + attributes)
|
||||
{ composite: 'research-map-plus-grid', id: 'grid -> composite (groups-filtered)',
|
||||
send: [`${PD}/people-groups-grid/people-groups-grid.vue`, DOM_SEND('groups-filtered')],
|
||||
recv: [`${PD}/research-map-plus-grid/research-map-plus-grid.vue`, DOM_LISTEN('groups-filtered')],
|
||||
built: [['people-groups-grid', '"groups-filtered"'], ['research-map-plus-grid', '"groups-filtered"']] },
|
||||
{ composite: 'research-map-plus-grid', id: 'map -> composite (map-filtered)',
|
||||
send: [`${PD}/doxa-research-map/research-map.vue`, DOM_SEND('map-filtered')],
|
||||
recv: [`${PD}/research-map-plus-grid/research-map-plus-grid.vue`, DOM_LISTEN('map-filtered')],
|
||||
built: [['doxa-research-map', '"map-filtered"'], ['research-map-plus-grid', '"map-filtered"']] },
|
||||
{ composite: 'research-map-plus-grid', id: 'composite -> map (externalFilters)',
|
||||
send: [`${PD}/research-map-plus-grid/research-map-plus-grid.vue`, LIT('externalFilters')],
|
||||
recv: [`${PD}/doxa-research-map/research-map.vue`, LIT('externalFilters')],
|
||||
built: [['research-map-plus-grid', 'externalFilters'], ['doxa-research-map', 'externalFilters']] },
|
||||
{ composite: 'research-map-plus-grid', id: 'composite -> map (fitSlugs)',
|
||||
send: [`${PD}/research-map-plus-grid/research-map-plus-grid.vue`, LIT('fitSlugs')],
|
||||
recv: [`${PD}/doxa-research-map/research-map.vue`, /'fitSlugs' in raw/],
|
||||
built: [['research-map-plus-grid', 'fitSlugs'], ['doxa-research-map', 'fitSlugs']] },
|
||||
...[['wagf-region', 'wagfRegion'], ['religion-family', 'religionFamily'], ['primary-language', 'primaryLanguage']].map(([attr, prop]) => ({
|
||||
composite: 'research-map-plus-grid', id: `composite -> grid (attribute ${attr})`,
|
||||
send: [`${PD}/research-map-plus-grid/research-map-plus-grid.vue`, new RegExp(`setAttribute\\(\\s*'${esc(attr)}'`)],
|
||||
recv: [`${PD}/people-groups-grid/people-groups-grid.vue`, new RegExp(`^\\s*${prop}\\s*:\\s*\\{\\s*type`, 'm')],
|
||||
built: [['research-map-plus-grid', `"${attr}"`], ['people-groups-grid', prop]],
|
||||
})),
|
||||
// country-posters: tabs (window event scoped by instanceId)
|
||||
{ composite: 'country-posters', id: 'wall "Show on map" -> composite',
|
||||
send: [`${PD}/country-posters/country-poster-wall.vue`, /event-name="doxa-country-posters:show-on-map"/],
|
||||
recv: [`${PD}/country-posters/country-posters-mfe.vue`, /['"]doxa-country-posters:show-on-map['"]/],
|
||||
built: [['country-posters', 'doxa-country-posters:show-on-map']] },
|
||||
// my-upg-100-list: tabs
|
||||
{ composite: 'my-upg-100-list', id: 'wall "Show on map" -> composite',
|
||||
send: [`${PD}/my-upg-100-list/poster-wall.vue`, /event-name="doxa-upg-100-list:show-on-map"/],
|
||||
recv: [`${PD}/my-upg-100-list/upg-100-mfe.vue`, DOM_LISTEN('doxa-upg-100-list:show-on-map')],
|
||||
built: [['my-upg-100-list', 'doxa-upg-100-list:show-on-map']] },
|
||||
{ composite: 'my-upg-100-list', id: 'composite -> map (doxa:select-zone)',
|
||||
send: [`${PD}/my-upg-100-list/upg-100-mfe.vue`, DOM_SEND('doxa:select-zone')],
|
||||
recv: [`${PD}/my-upg-100-list/upg-100-list.vue`, DOM_LISTEN('doxa:select-zone')],
|
||||
built: [['my-upg-100-list', '"doxa:select-zone"']] },
|
||||
// regions-dashboard: tabs
|
||||
...['region-selected', 'country-selected'].map((n) => ({
|
||||
composite: 'regions-dashboard', id: `list -> composite (${n})`,
|
||||
send: [`${PD}/regions-dashboard/regions-list.vue`, DOM_SEND(n)],
|
||||
recv: [`${PD}/regions-dashboard/regions-mfe.vue`, DOM_LISTEN(n)],
|
||||
built: [['regions-dashboard', `"${n}"`]],
|
||||
})),
|
||||
{ composite: 'regions-dashboard', id: 'composite -> dashboards (doxa-regions-dashboard:select)',
|
||||
send: [`${PD}/regions-dashboard/regions-mfe.vue`, /SELECT_EVENT = 'doxa-regions-dashboard:select'[\s\S]*dispatchEvent\(new CustomEvent\(SELECT_EVENT/],
|
||||
recv: [`${PD}/regions-dashboard/region-dashboards.vue`, /SELECT_EVENT = 'doxa-regions-dashboard:select'[\s\S]*addEventListener\(SELECT_EVENT/],
|
||||
built: [['regions-dashboard', 'doxa-regions-dashboard:select']] },
|
||||
// gospel-resources (gospel-dashboard): children compiled in, provide/inject
|
||||
{ composite: 'gospel-resources', id: 'map -> tables (composedFilter)',
|
||||
send: [`${PD}/gospel-resources/resources-map.vue`, /composedFilter\.value\s*=/],
|
||||
recv: [`${PD}/gospel-resources/data-tables.vue`, /watch\(\s*composedFilter/],
|
||||
built: [['gospel-resources', '"composedFilter"']] },
|
||||
]
|
||||
|
||||
// Composites whose children deliberately exchange nothing. The reason is printed on every run.
|
||||
const NO_LINKS = {
|
||||
'stack-demo': 'a demonstration stack: page knobs only; dashboard-plus-grid is the linked version',
|
||||
}
|
||||
|
||||
// ── D: late getCurrentInstance() sites already routed to an owner ─────────────────────────────
|
||||
const LATE_KNOWN = {
|
||||
// emitFilterState runs from a watcher, so `map-filtered` is never dispatched (map -> grid dead).
|
||||
[`${PD}/doxa-research-map/research-map.vue#emitFilterState`]: 'doxa-research-map owner, routed via dx-shepherd 2026-09-26',
|
||||
// reflectDir runs from watch(textDir): a host switching lang to ar AFTER mount gets no dir=rtl.
|
||||
[`${PD}/dashboard-plus-grid/dashboard-plus-grid.vue#reflectDir`]: 'composite owner, routed via dx-shepherd 2026-09-26',
|
||||
[`${PD}/research-map-plus-grid/research-map-plus-grid.vue#reflectDir`]: 'composite owner, routed via dx-shepherd 2026-09-26',
|
||||
}
|
||||
|
||||
const fails = []
|
||||
const known = []
|
||||
const read = (rel) => (existsSync(join(ROOT, rel)) ? readFileSync(join(ROOT, rel), 'utf8') : null)
|
||||
const bundleCache = {}
|
||||
const bundle = (b) => (bundleCache[b] ??= read(`public/${TREE_NAME}/${b}/${b}.js`))
|
||||
|
||||
// A + B
|
||||
for (const l of LINKS) {
|
||||
const tag = `${l.composite}: ${l.id}`
|
||||
for (const [side, [file, re]] of [['sender', l.send], ['receiver', l.recv]]) {
|
||||
const src = read(file)
|
||||
if (src == null) fails.push(`${tag} — ${side} file is gone: ${file}`)
|
||||
else if (!re.test(src)) fails.push(`${tag} — the ${side} no longer matches ${re} in ${file} (renamed on one side?)`)
|
||||
}
|
||||
for (const [b, lit] of l.built) {
|
||||
const js = bundle(b)
|
||||
if (js == null) fails.push(`${tag} — no built bundle public/${TREE_NAME}/${b}/${b}.js`)
|
||||
else if (!js.includes(lit)) fails.push(`${tag} — built ${b}.js does not contain ${lit} (stale bundle, or renamed)`)
|
||||
}
|
||||
}
|
||||
|
||||
// C
|
||||
const composites = []
|
||||
for (const b of readdirSync(join(ROOT, PD))) {
|
||||
let files = []
|
||||
try { files = readdirSync(join(ROOT, PD, b)) } catch { continue }
|
||||
for (const f of files.filter((f) => f.endsWith('.meta.json'))) {
|
||||
try { if (JSON.parse(readFileSync(join(ROOT, PD, b, f), 'utf8')).type === 'nested-elements-mfe') composites.push(b) } catch { /* not ours to judge */ }
|
||||
}
|
||||
}
|
||||
if (!composites.length) fails.push('found no capability-4 composites (vacuous run)')
|
||||
for (const c of composites) {
|
||||
const n = LINKS.filter((l) => l.composite === c).length
|
||||
if (!n && !NO_LINKS[c]) fails.push(`${c}: unmapped composite — add its links to LINKS, or an entry to NO_LINKS saying why it has none`)
|
||||
}
|
||||
|
||||
// D
|
||||
function* sources(dir) {
|
||||
for (const e of readdirSync(join(ROOT, dir), { withFileTypes: true })) {
|
||||
const rel = `${dir}/${e.name}`
|
||||
if (e.isDirectory()) { if (!/node_modules|^\./.test(e.name) && !/\/(scripts|dist)$/.test(rel)) yield* sources(rel) }
|
||||
else if (/\.(vue|js)$/.test(e.name)) yield rel
|
||||
}
|
||||
}
|
||||
const HOOK = /\b(on(Before)?(Mounted|Unmounted|Update|Updated)|onActivated|onDeactivated)\s*\(/
|
||||
for (const rel of [...sources(PD), ...sources('library')]) {
|
||||
const lines = readFileSync(join(ROOT, rel), 'utf8').split('\n')
|
||||
lines.forEach((line, i) => {
|
||||
if (!/getCurrentInstance\(\)/.test(line) || /^\s*(\/\/|\*|import\b)/.test(line)) return
|
||||
// Walk the enclosing blocks outward by indentation.
|
||||
let ind = line.search(/\S/)
|
||||
let verdict = 'top'
|
||||
let fnName = ''
|
||||
// A one-liner opens its function on the SAME line as the call: read that prefix first.
|
||||
const pre = line.slice(0, line.indexOf('getCurrentInstance()'))
|
||||
const same = pre.match(/function\s+([A-Za-z0-9_$]+)\s*\(/)
|
||||
if (HOOK.test(pre) || /\bsetup\s*\(/.test(pre)) verdict = 'ok'
|
||||
else if (same) { verdict = /^use[A-Z]/.test(same[1]) ? 'ok' : 'late'; fnName = same[1] }
|
||||
else if (/=>/.test(pre) || /\bfunction\s*\(/.test(pre)) { verdict = 'late'; fnName = (pre.match(/(?:const|let)\s+([A-Za-z0-9_$]+)\s*=/) || [])[1] || '(anonymous)' }
|
||||
for (let j = i - 1; verdict === 'top' && j >= 0 && ind > 0; j--) {
|
||||
const l = lines[j]
|
||||
if (!l.trim() || /^\s*(\/\/|\*)/.test(l)) continue
|
||||
const li = l.search(/\S/)
|
||||
if (li >= ind) continue
|
||||
ind = li
|
||||
if (HOOK.test(l) || /\bsetup\s*\(/.test(l)) { verdict = 'ok'; break }
|
||||
const m = l.match(/function\s+([A-Za-z0-9_$]+)\s*\(/)
|
||||
if (m) { if (/^use[A-Z]/.test(m[1])) { verdict = 'ok'; break } verdict = 'late'; fnName = m[1]; break }
|
||||
if (/=>\s*\{?\s*$/.test(l) || /\bfunction\s*\(/.test(l)) { verdict = 'late'; fnName = '(anonymous)'; break }
|
||||
}
|
||||
if (verdict !== 'late') return
|
||||
const key = `${rel}#${fnName}`
|
||||
const where = `${rel}:${i + 1} in ${fnName}()`
|
||||
if (LATE_KNOWN[key]) known.push(`KNOWN [late getCurrentInstance] ${where} — owner: ${LATE_KNOWN[key]}`)
|
||||
else fails.push(`${where}: getCurrentInstance() inside a plain function returns null when it runs from a watcher or an event. Capture \`const inst = getCurrentInstance()\` in setup and use inst here.`)
|
||||
})
|
||||
}
|
||||
for (const k of Object.keys(LATE_KNOWN)) {
|
||||
if (!known.some((m) => m.includes(k.split('#')[0]) && m.includes(k.split('#')[1] + '()'))) {
|
||||
fails.push(`LATE_KNOWN entry no longer matches anything: ${k} — it was fixed; delete the entry so the gate enforces it`)
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`COMPOSITE-INTEROP: ${composites.length} composite(s), ${LINKS.length} link(s) checked in source and in the built tree (${TREE_NAME})`)
|
||||
for (const [c, why] of Object.entries(NO_LINKS)) console.log(` · ${c}: no links, by design — ${why}`)
|
||||
for (const k of known) console.log(` ${k}`)
|
||||
if (fails.length) {
|
||||
console.log(`\nCOMPOSITE-INTEROP: FAIL (${fails.length})`)
|
||||
for (const f of fails) console.log(` - ${f}`)
|
||||
process.exit(1)
|
||||
}
|
||||
console.log('COMPOSITE-INTEROP: PASS — every link is present on both ends, in source and in the built bundles')
|
||||
|
|
@ -32,6 +32,7 @@
|
|||
"check:composite": "node internal/check-composite-freshness.mjs",
|
||||
"check:clustering-modes": "node plugin-designs/doxa-research-map/scripts/check-clustering-modes.mjs",
|
||||
"check:composite-embed-knobs": "node internal/check-composite-embed-knobs.mjs",
|
||||
"check:composite-interop": "node internal/check-composite-interop.mjs",
|
||||
"check:composite-locale-leaks": "node plugin-designs/research-map-plus-grid/scripts/check-composite-locale-leaks.mjs",
|
||||
"check:composite-reg": "node internal/check-composite-registration.mjs",
|
||||
"check:composite-srcs": "node internal/check-composite-srcs.mjs",
|
||||
|
|
@ -244,6 +245,7 @@
|
|||
"check:composite": "a COMPOSITE is a COPY taken at its own build time: fails if a nested child's source is newer than the parent's bundle. NOT in build:unlocked on purpose — there everything was just built, so it could never catch the real case (a PARTIAL build). Run it before PUBLISH.",
|
||||
"check:composite-embed-knobs": "a composite must pass the HOST's tk and provider down to the map it nests, proven on a genuinely foreign origin (tree served on one port, partner page on another). Our own leaves bake a token and assign window.MAP_TOKEN, so this only ever breaks where there is no leaf - the partner page the embed snippet is written for, and the case nothing else tested. Keyless maplibre asserts ZERO requests to api.mapbox.com, because 'it rendered' would pass on a build that quietly fell back with a baked key.",
|
||||
"check:clustering-modes": "every option in the research map's clustering picker must DRAW something different. Choosing 'Native' changed nothing: mapStore never declared clusteringMode, so the picker wrote to a property nothing watched, AND the Clusters checkbox force-set 'mst' regardless of the store, so even a fixed picker would have snapped back on the next tick. Asserts on what the GL style RENDERS (circles, count labels, lines), not on state - 'clusteringMode === mapbox' would pass on a build still drawing MST. Also asserts no orphan layers between modes, and that MST and Network differ, since two labels for one behaviour is the same defect as a dead option.",
|
||||
"check:composite-interop": "every link between a capability-4 composite's children exists on BOTH ends, headless: the event, attribute or config-key name is sent in the sender file and listened for in the receiver (a rename on one side fails, naming both files), the same literal is in every built bundle that ships an end, every composite is mapped (or says why it has no links), and no host lookup calls getCurrentInstance() inside a plain function, where Vue returns null when it runs from a watcher. Exists because Simple Research map -> grid died that way while a synthetic-event proof passed. Both ends existing is necessary, not sufficient: the runtime half is check:gallery-embed's linked phase.",
|
||||
"check:composite-locale-leaks": "a composite rendered in a non-English locale must not show a string the catalogue already translates. The criterion is a LOOKUP, not a category: English catalogue holds the string as a value, this locale holds a different value for that key, the page still renders English. The previous sweep split strings into chrome/data/third-party and scored single words like 'Region' and 'Prayer' as data, so 28 leaks on Big Picture and 41 on Simple Research counted as zero. Surfaces that a lookup cannot judge - vendor map chrome, table DATA cells, facet-rail values - are suppressed by NAME with a stated reason, never by word.",
|
||||
"check:coverage-consistent": "the gospel-resources surfaces must agree: no resource on an ISO RESERVED code (und/mul/mis/zxx), every resourced code NAMED, table reach recounted off the pins, no invented URL, tiers reconcile. Static, sub-second. Self-test: `bun run check:coverage-consistent:poison`, all 12 clauses (A-L) must fail. Clause L is the one that is not about consistency: it fails when the link-verification record goes stale, because 'link checked' is a claim about the present tense. Written after the same drift defect was found by hand three times.",
|
||||
"check:csv-shape": "the export must OPEN as a table: header on line 0, every row matching. Run the `check:csv-shape:self-test` script for the NEGATIVE test (forces the provenance comment back to line 0 IN MEMORY and requires this gate to REJECT it) — the header used to claim 'NEGATIVE-TESTED' with no way to re-run it.",
|
||||
|
|
|
|||
Loading…
Reference in a new issue