mirror of
https://github.com/got-feedBack/feedBack.git
synced 2026-07-21 20:31:21 +00:00
The first carve, and deliberately the riskiest: app.js IS the plugin loader (the
R0 host rails), so it goes first while the module graph is still one edge deep.
static/js/plugin-loader.js (829 lines) — bodies VERBATIM. app.js 12,217 → 11,439.
Core's first `static/js/` module, exactly as constitution II anticipates.
CLOSURE (measured with acorn, not regex — brace-matching stripped source drifted):
the block at app.js:11246-12031 is contiguous and self-contained. It needs only
TWO things from the rest of app.js, and exports only TWO:
exports: loadPlugins (the window contract), bootstrapPluginsAndUi (boot)
inbound: window.showScreen — already the public host contract (constitution II),
so it is called through `window`, not re-coupled as an import
_populateVizPicker — injected via configurePluginLoader()
WHY A SEAM, NOT AN IMPORT. plugin-loader must not import app.js: app.js imports
it, so that would close a cycle. I checked whether _populateVizPicker could just
move into the module instead (which would delete the seam entirely) — it drags 9
further symbols (_canRun3D, _autoMatchViz, _showPromotionNag, …), i.e. a whole
viz cluster. That is its own carve, so the seam stays.
THE SEAM'S DEFAULT IS LOUD, ON PURPOSE. A no-op stub is the classic silent
failure for this pattern (see the editor's setHostHooks trap, hit twice): drop the
wiring call and the loader keeps working while the viz picker quietly stops
refreshing — no test, no boot check says a word. The default now console.errors,
so the smoke harness catches it. VERIFIED BY BITE TEST: removing
configurePluginLoader() from app.js surfaces
"[plugin-loader] host seam not configured" at boot. The seam IS exercised on the
plugin-startup path, so an unwired hook cannot pass silently.
no-cycle is now LIVE on core's own graph for the first time. eslint.config.js
gains `static/app.js` + `static/js/**` to the module block — app.js now `import`s,
so parsing it as a script would be a syntax error. VERIFIED BY BITE TEST: making
plugin-loader import app.js back fails with "Dependency cycle detected".
HARNESSES (the R3a note said budget one conversion per carve — it was five):
retargeted capability_inspector_nav, plugin_hydration_wipe,
plugin_loader_script_type, plugin_style_injection, legacy_shim_hits (SPLIT — one
test needs the loader, one still needs app.js) + test_plugin_runtime_idempotence.
legacy_shim_hits was missed by a symbol-name grep because it greps for a code
STRING; only the failing run found it. test_capability_events' NEGATIVE asserts
now span app.js + the loader — carving code out of app.js would otherwise make
them vacuous instead of failing.
VERIFIED: A/B against origin/main in two browsers — mounted plugin screens, 14
loaded plugin scripts, the 3 module plugins injected as <script type="module">,
37 capability participants, 14 shims, window.loadPlugins: IDENTICAL, zero
console/page errors on both. /static/js/plugin-loader.js serves 200; R0 rails
intact (src/main.js 200, conditional GET 304, script_type passthrough).
pytest 2396, node 1032/1032, ESLint 0, Codex 0.
Codex preflight caught a REAL [P1] first pass: static/js/plugin-loader.js was
untracked, so a checkout would have served an app.js importing a nonexistent
module — a failed static import kills the whole module and every window handler
with it. Now tracked.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
74 lines
3.6 KiB
JavaScript
74 lines
3.6 KiB
JavaScript
// Flat ESLint config — MAINTAINER / CI ONLY. Never runs on the serve or Docker
|
|
// path (constitution Principle I: dev-only tooling is exempt, same category as
|
|
// scripts/build-tailwind.sh). It enforces the module-migration guardrails:
|
|
//
|
|
// * max-lines — the 1,500-line size norm, as a WARNING ratchet. Legacy
|
|
// monoliths warn (the "this is over the norm, split it" signal) and shrink
|
|
// as the refactor lands; warnings do not fail CI. Genuinely-large files are
|
|
// exempted below, mirroring the signed register in docs/size-exemptions.md.
|
|
// * import-x/no-unresolved + no-cycle — module hygiene, scoped to the real
|
|
// ES-module graphs the refactor produces (a plugin's src/ tree, .mjs
|
|
// tests). no-unresolved (a HARD error) catches broken import paths;
|
|
// no-cycle enforces the downward-only layering rule. Core's classic scripts
|
|
// have no import graph, so both are dormant today and become live gates the
|
|
// moment module code appears — validated against the first real module
|
|
// plugin (R1 pilot).
|
|
|
|
const importX = require('eslint-plugin-import-x');
|
|
|
|
// Per-file size ceilings — a mirror of docs/size-exemptions.md (canonical).
|
|
// Keep in sync; each entry corresponds to a signed row in the register.
|
|
const SIZE_EXEMPTIONS = [
|
|
{ files: ['**/static/capabilities.js'], max: 1600 },
|
|
{ files: ['**/plugins/capability_inspector/screen.js'], max: 100000 },
|
|
{ files: ['**/plugins/folder_library/screen.js'], max: 100000 },
|
|
];
|
|
|
|
const sizeRule = (max) => ['warn', { max, skipBlankLines: false, skipComments: false }];
|
|
|
|
module.exports = [
|
|
{
|
|
ignores: [
|
|
'node_modules/**',
|
|
'static/vendor/**',
|
|
'plugins/**/assets/vendor/**',
|
|
'**/*.min.js',
|
|
'static/tailwind.min.css',
|
|
],
|
|
},
|
|
// Size norm across all first-party JS. Classic scripts are parsed as
|
|
// scripts (no import/export); module files get their own block below.
|
|
{
|
|
files: ['**/*.js', '**/*.cjs'],
|
|
languageOptions: { ecmaVersion: 'latest', sourceType: 'script' },
|
|
rules: { 'max-lines': sizeRule(1500) },
|
|
},
|
|
// ES-module graphs (a plugin's src/ tree, .mjs tests, core's own static/js/
|
|
// tree): module parsing + the acyclic-imports hard gate + the size norm. A
|
|
// migrated bundled plugin's entry `import './src/main.js'` screen.js must
|
|
// parse as a module — add its glob here in that plugin's migration PR
|
|
// (classic screen.js stays a script).
|
|
//
|
|
// `static/app.js` is listed explicitly: it is served as
|
|
// <script type="module"> (R3a) and now `import`s its carved-out modules, so
|
|
// parsing it as a script would be a syntax error. It is the ENTRY of core's
|
|
// module graph, which is what makes no-cycle meaningful here — a carved
|
|
// module that imports app.js back would close a cycle and fail this gate.
|
|
{
|
|
files: ['**/src/**/*.js', '**/*.mjs', 'static/app.js', 'static/js/**/*.js'],
|
|
languageOptions: { ecmaVersion: 'latest', sourceType: 'module' },
|
|
plugins: { 'import-x': importX },
|
|
// v4 flat-config resolver (resolver-next + createNodeResolver). Without
|
|
// it the import rules silently skip imports they can't resolve.
|
|
settings: { 'import-x/resolver-next': [importX.createNodeResolver()] },
|
|
rules: {
|
|
'max-lines': sizeRule(1500),
|
|
'import-x/no-unresolved': 'error',
|
|
'import-x/no-cycle': 'error',
|
|
},
|
|
},
|
|
// Signed size exemptions (docs/size-exemptions.md) — raise the ceiling so
|
|
// registered files don't warn below it.
|
|
...SIZE_EXEMPTIONS.map(({ files, max }) => ({ files, rules: { 'max-lines': sizeRule(max) } })),
|
|
];
|