Measure engine coupling and run the contract suite on the real engines

scripts/report-engine-coupling.mjs counts the engine-specific code left
outside src/globe/cesium and src/globe/navara: layer renderer lines per
engine, Cesium imports, `viewer` mentions, engine-name checks, raw Navara
reads, Navara ground passes, Cesium SDK loads and ScreenSpaceEventHandler
users. engineCoupling.test.mjs fails npm test when a count grows past
scripts/engine-coupling-baseline.json, so each step towards drawing
through the globe contract can only lower them.

scripts/qa-contract-suite.mjs runs src/globe/testing/contractSuite.mjs in
headless Chrome on the Cesium and Navara of a keyless dev server, which
until now only ran on the fake engine. The page imports the suite from the
dev server, starts at a fixed view, waits for the camera to rest and lets
the pointer through the panels. Cesium passes 57 of 57 checks, Navara 64
of 67; docs/CURRENT-STATE.md names the three that fail.
This commit is contained in:
German Khokhlov committed 2026-11-07 01:08:15 +04:00
1 parent 92c8f8d9da
commit 9daa4bd2db
6 files changed
+448 -3

No files matched your search

+232
View File
@@ -0,0 +1,232 @@
#!/usr/bin/env node
/**
* How tightly the application is still tied to one globe engine: counts of
* engine-specific code outside the engines (src/globe/cesium, src/globe/navara).
* The renderer-abstraction work drives every number down; none may grow.
* engineCoupling.test.mjs (npm test) compares a fresh count with
* scripts/engine-coupling-baseline.json.
*
* Only production sources count: `.js` and `.vue` under src. Tests, fixtures
* and test doubles are `.mjs`.
*
* node scripts/report-engine-coupling.mjs # table
* node scripts/report-engine-coupling.mjs --json
* node scripts/report-engine-coupling.mjs --write-baseline
*/
import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath, pathToFileURL } from 'node:url';
const ROOT = fileURLToPath(new URL('..', import.meta.url));
export const BASELINE_PATH = path.join(
ROOT,
'scripts',
'engine-coupling-baseline.json',
);
const inEngine = (file) =>
file.startsWith('src/globe/cesium/') || file.startsWith('src/globe/navara/');
const inGlobe = (file) => file.startsWith('src/globe/');
const isLayerRenderer = (engine) => (file) =>
file.startsWith('src/layers/') &&
new RegExp(`/render/${engine}([^/]*\\.js|/.+\\.js)$`).test(file);
const isRenderer = (file) => /\/render\//.test(file);
const lines = (text) => text.split('\n');
const countLines = (text, pattern) =>
lines(text).filter((line) => pattern.test(line)).length;
const countMatches = (text, pattern) =>
(text.match(new RegExp(pattern.source, 'g')) ?? []).length;
/**
* The metrics, in report order. `measure(files)` gets every production
* source as `{ file, text }` (file relative to the repository, with `/`).
*/
export const ENGINE_COUPLING_METRICS = Object.freeze([
{
id: 'cesiumLayerRendererLines',
label: 'Lines in src/layers/**/render/cesium*',
measure: (files) =>
files
.filter(({ file }) => isLayerRenderer('cesium')(file))
.reduce((sum, { text }) => sum + lines(text).length, 0),
},
{
id: 'navaraLayerRendererLines',
label: 'Lines in src/layers/**/render/navara*',
measure: (files) =>
files
.filter(({ file }) => isLayerRenderer('navara')(file))
.reduce((sum, { text }) => sum + lines(text).length, 0),
},
{
id: 'cesiumImportFiles',
label: "Files importing 'cesium' outside src/globe/cesium and render/",
measure: (files) =>
files.filter(
({ file, text }) =>
!file.startsWith('src/globe/cesium/') &&
!isRenderer(file) &&
/from\s+['"]cesium['"]|import\(\s*['"]cesium['"]\s*\)/.test(text),
).length,
},
{
id: 'viewerLines',
label: 'Lines naming `viewer` outside src/globe/cesium',
measure: (files) =>
files
.filter(({ file }) => !file.startsWith('src/globe/cesium/'))
.reduce((sum, { text }) => sum + countLines(text, /\bviewer\b/), 0),
},
{
id: 'navaraKindChecks',
label: "`kind === 'navara'` checks outside src/globe",
measure: (files) =>
files
.filter(({ file }) => !inGlobe(file))
.reduce(
(sum, { text }) =>
sum + countMatches(text, /kind\s*[!=]==?\s*['"]navara['"]/),
0,
),
},
{
id: 'engineSniffCalls',
label: 'isGlobeEngine / engineFromViewer calls outside src/globe',
measure: (files) =>
files
.filter(({ file }) => !inGlobe(file))
.reduce(
(sum, { text }) =>
sum + countMatches(text, /\b(?:isGlobeEngine|engineFromViewer)\(/),
0,
),
},
{
id: 'navaraViewsReads',
label: 'Raw Navara reads through navaraViews',
measure: (files) =>
files
.filter(({ file }) => !inEngine(file))
.reduce(
(sum, { text }) => sum + countMatches(text, /\bnavaraViews\.get\(/),
0,
),
},
{
id: 'groundEffectPasses',
label: 'Navara ground passes (defineGroundEffect calls)',
measure: (files) =>
files.reduce(
(sum, { text }) =>
sum + countMatches(text, /\bdefineGroundEffect\(\s*\{/),
0,
),
},
{
id: 'cesiumSdkLoads',
label: 'Cesium SDK loads outside the engines and loadCesium.js',
measure: (files) =>
files
.filter(
({ file }) => !inEngine(file) && file !== 'src/app/loadCesium.js',
)
.reduce(
(sum, { text }) =>
sum + countMatches(text, /\b(?:loadCesiumSdk|whenCesiumSdk)\(/),
0,
),
},
{
id: 'screenSpaceHandlerFiles',
label: 'Files in src/layers and src/data using ScreenSpaceEventHandler',
measure: (files) =>
files.filter(
({ file, text }) =>
(file.startsWith('src/layers/') || file.startsWith('src/data/')) &&
/ScreenSpaceEventHandler/.test(text),
).length,
},
{
id: 'engineNameLines',
label:
"Lines with an engine name literal ('navara', 'cesium') outside src/globe",
measure: (files) =>
files
.filter(({ file }) => !inGlobe(file))
.reduce(
(sum, { text }) =>
sum + countLines(text, /['"](?:navara|cesium)['"]/),
0,
),
},
]);
/** Production sources under `root`/src: `.js` and `.vue`. */
export function readProductionSources(root = ROOT) {
const files = [];
const visit = (directory) => {
for (const entry of fs.readdirSync(directory, { withFileTypes: true })) {
const absolute = path.join(directory, entry.name);
if (entry.isDirectory()) visit(absolute);
else if (entry.isFile() && /\.(?:js|vue)$/.test(entry.name))
files.push({
file: path.relative(root, absolute).split(path.sep).join('/'),
text: fs.readFileSync(absolute, 'utf8'),
});
}
};
visit(path.join(root, 'src'));
return files.sort((a, b) => a.file.localeCompare(b.file));
}
/** Every metric's value for the sources under `root`. */
export function measureEngineCoupling(root = ROOT) {
const files = readProductionSources(root);
return Object.fromEntries(
ENGINE_COUPLING_METRICS.map(({ id, measure }) => [id, measure(files)]),
);
}
/** The metrics that grew past `baseline`, with both values. */
export function couplingGrowth(measured, baseline) {
return ENGINE_COUPLING_METRICS.filter(
({ id }) => !(measured[id] <= (baseline[id] ?? -Infinity)),
).map(({ id, label }) => ({
id,
label,
measured: measured[id],
baseline: baseline[id],
}));
}
export function readBaseline(file = BASELINE_PATH) {
return JSON.parse(fs.readFileSync(file, 'utf8'));
}
function main(args) {
const measured = measureEngineCoupling();
if (args.includes('--write-baseline')) {
const note = args.find((arg) => arg.startsWith('--note='))?.slice(7);
fs.writeFileSync(
BASELINE_PATH,
`${JSON.stringify({ metrics: measured, ...(note ? { measuredAt: note } : {}) }, null, 2)}\n`,
);
}
if (args.includes('--json')) {
console.log(JSON.stringify(measured, null, 2));
return;
}
const baseline = fs.existsSync(BASELINE_PATH) ? readBaseline().metrics : {};
for (const { id, label } of ENGINE_COUPLING_METRICS) {
const was = baseline[id];
const change =
was === undefined
? ''
: ` (baseline ${was}, ${measured[id] - was >= 0 ? '+' : ''}${measured[id] - was})`;
console.log(`${String(measured[id]).padStart(7)} ${label}${change}`);
}
}
if (import.meta.url === pathToFileURL(process.argv[1] ?? '').href)
main(process.argv.slice(2));