check:no-cross-plugin-imports: a plugin may not reach into another plugin's private folder
research-dashboard and dashboard-plus-grid imported '../doxa-research-map/src/colors/…' — another plugin's private code — which is how two copies of the same palette drifted (C1a moves them to library/colors). The gate refuses any relative import that resolves into plugin-designs/<other>/ below that bundle's root; a composite mounting another bundle's PUBLIC profile .vue (the capability-4 pattern) stays allowed. It FAILs on main (the 4 palette imports C1a removes) and PASSes with C1a; --self-test in gates:post.
This commit is contained in:
parent
6263c250ec
commit
de5a3fdf80
2 changed files with 63 additions and 1 deletions
59
internal/check-no-cross-plugin-imports.mjs
Normal file
59
internal/check-no-cross-plugin-imports.mjs
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
#!/usr/bin/env node
|
||||
/**
|
||||
* check-no-cross-plugin-imports — a plugin imports another plugin only through library/, never by reaching
|
||||
* into its folder.
|
||||
*
|
||||
* WHY (2026-09-26, regions' C1a): research-dashboard and dashboard-plus-grid imported
|
||||
* '../doxa-research-map/src/…' — another plugin's PRIVATE folder. That couples two owners' code invisibly
|
||||
* (a refactor inside doxa-research-map/src breaks a bundle its owner never builds), and it is how two
|
||||
* private copies of the same palette drifted apart. Shared code belongs in library/ (@map/…).
|
||||
*
|
||||
* RULE: in plugin-designs/<bundle>/**, a relative import (static, dynamic, export-from, import.meta.glob)
|
||||
* must not resolve into plugin-designs/<other>/ — except another bundle's PUBLIC profile component (a .vue at
|
||||
* that bundle's root), which is how a capability-4 composite mounts it. Anything deeper (src/, utils) is refused.
|
||||
* --self-test plants a crossing import and requires a FAIL.
|
||||
* EXIT 0 pass · 1 fail
|
||||
*/
|
||||
import { readFileSync, readdirSync, statSync } from 'node:fs'
|
||||
import { join, resolve, dirname, relative, sep } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
|
||||
const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..')
|
||||
const PD = join(ROOT, 'plugin-designs')
|
||||
const SPEC = /(?:import|export)\s[^'"`;]*?from\s*['"]([^'"]+)['"]|import\s*['"]([^'"]+)['"]|import\s*\(\s*['"]([^'"]+)['"]\s*\)|import\.meta\.glob\s*\(\s*['"]([^'"]+)['"]/g
|
||||
|
||||
/** [{file, spec, target}] for every relative import in `src` (at `file`) that lands in another bundle. */
|
||||
export function crossings(file, src) {
|
||||
const own = relative(PD, file).split(sep)[0]
|
||||
const out = []
|
||||
for (const m of src.matchAll(SPEC)) {
|
||||
const spec = m[1] || m[2] || m[3] || m[4]
|
||||
if (!spec || !spec.startsWith('.')) continue
|
||||
const abs = resolve(dirname(file), spec)
|
||||
const rel = relative(PD, abs)
|
||||
if (rel.startsWith('..')) continue // left plugin-designs/ (library/, data/): fine
|
||||
const parts = rel.split(sep)
|
||||
const target = parts[0]
|
||||
if (!target || target === own) continue
|
||||
// A composite mounting another bundle's PUBLIC profile (a .vue at that bundle's root) is composition,
|
||||
// the documented capability-4 pattern. Reaching any deeper (src/, utils, data) is the private coupling.
|
||||
if (parts.length === 2 && /\.vue$/.test(parts[1])) continue
|
||||
out.push({ file: relative(ROOT, file), spec, target })
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
if (process.argv.includes('--self-test')) {
|
||||
const f = join(PD, 'research-dashboard', 'x.vue')
|
||||
const hits = crossings(f, "import A from '../doxa-research-map/src/a.js'\nimport B from './src/b.js'\nimport C from '../../library/c.js'\nimport D from '../people-groups-grid/people-groups-grid.vue'")
|
||||
if (hits.length === 1 && hits[0].target === 'doxa-research-map') { console.log('NO-CROSS-PLUGIN-IMPORTS: SELF-TEST PASS — a reach into another plugin\'s src/ is caught; its own src/, library/ and another bundle\'s public profile .vue are not.'); process.exit(0) }
|
||||
console.error('NO-CROSS-PLUGIN-IMPORTS: SELF-TEST FAIL — ' + JSON.stringify(hits)); process.exit(1)
|
||||
}
|
||||
|
||||
const files = []
|
||||
const walk = (d) => { for (const e of readdirSync(d)) { if (e === 'node_modules' || e.startsWith('.')) continue; const p = join(d, e); const st = statSync(p); if (st.isDirectory()) walk(p); else if (/\.(vue|m?js)$/.test(e) && !/\/(scripts|data)\//.test(p.replace(/\\/g, '/'))) files.push(p) } }
|
||||
walk(PD)
|
||||
const bad = files.flatMap((f) => crossings(f, readFileSync(f, 'utf8')))
|
||||
for (const b of bad) console.log(` FAIL ${b.file}: imports '${b.spec}' from another plugin (${b.target}); move the shared code to library/ and import it as @map/…`)
|
||||
if (bad.length) { console.log(`NO-CROSS-PLUGIN-IMPORTS: FAIL — ${bad.length} import(s) reach into another plugin's folder.`); process.exit(1) }
|
||||
console.log(`NO-CROSS-PLUGIN-IMPORTS: PASS — ${files.length} plugin source file(s); none imports from another plugin's folder.`)
|
||||
|
|
@ -129,6 +129,8 @@
|
|||
"check:no-codename": "node internal/check-no-internal-codename.mjs",
|
||||
"check:no-conflict-markers": "node internal/check-no-conflict-markers.mjs",
|
||||
"check:no-conflict-markers:poison": "node internal/check-no-conflict-markers.mjs --poison",
|
||||
"check:no-cross-plugin-imports": "node internal/check-no-cross-plugin-imports.mjs",
|
||||
"check:no-cross-plugin-imports:self-test": "node internal/check-no-cross-plugin-imports.mjs --self-test",
|
||||
"check:no-eval": "node internal/check-no-eval.mjs",
|
||||
"check:no-free-identifiers": "node plugin-designs/research-map-plus-grid/scripts/check-no-free-identifiers.mjs",
|
||||
"check:no-orphan-gates": "node internal/check-no-orphan-gates.mjs",
|
||||
|
|
@ -203,7 +205,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 check:one-palette:self-test",
|
||||
"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 check:no-cross-plugin-imports check:no-cross-plugin-imports: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",
|
||||
"internet:adm1": "node plugin-designs/cities-internet/scripts/build-adm1-internet.mjs",
|
||||
"library-index": "node internal/generate-library-index.mjs",
|
||||
|
|
@ -327,6 +329,7 @@
|
|||
"check:no-abs-paths": "no /home/… or /Users/… in any TRACKED file (a clone must work on any machine)",
|
||||
"check:no-codename": "the development-era INTERNAL CODE NAME may not ship in any TRACKED file. Same hand-off family as check:no-abs-paths: meaningless to the Doxa team who inherit this, and invisible to every other gate (none read for vocabulary). Deliberately does NOT fail on the same letters appearing as the unix USERNAME inside a /home/… path — that is a portability defect check:no-abs-paths already reports better, and two gates disagreeing about one string teaches everyone to ignore both.",
|
||||
"check:no-conflict-markers": "Nothing tracked may carry an unresolved merge. Exists because a merge from this worktree committed a BUILT bundle with 21 conflict markers - both sides concatenated, 1.13MB - and every gospel leaf rendered dead while the SRI hash matched and every request returned 200. The cause was a landing script that resolved one expected file then ran \"git add -A public\", staging every other file including the ones still holding markers. For a built file: take either side, REBUILD, commit the rebuild, then run this before pushing. --poison for the negative test.",
|
||||
"check:no-cross-plugin-imports": "no plugin imports from another plugin's folder (src/, utils, data) by a relative path; shared code goes through library/ (@map/\u2026). A composite may mount another bundle's PUBLIC profile .vue. --self-test plants a crossing import.",
|
||||
"check:no-eval": "no built bundle contains new Function or eval (a strict CSP without 'unsafe-eval' must work), and vue-i18n in JIT mode renders real catalogue messages in en and ar with eval blocked. Headless.",
|
||||
"check:no-free-identifiers": "every name a .vue script uses is imported, declared, a parameter or a known global (scope-aware, across plugin-designs and library). A free identifier compiles and then throws at runtime in exactly the path nobody clicked.",
|
||||
"check:packet-browser": "the packet the READER gets — the legend's Export > Prayer packet path, proving the shared renderer runs in a BROWSER. Asserts the same 12 points / 12 wheel labels / page-1 geography / print layer as check:packet-render, which it did not until 2026-09-24. CONDITIONAL: SKIPs without a server over the repo root (python3 -m http.server 4898) and a reachable records API.",
|
||||
|
|
|
|||
Loading…
Reference in a new issue