Files
gods-eye-view/scripts/report-engine-coupling.mjs
T
German Khokhlov c73c5382d4 Draw weather, wind and Bhote Koshi through engine.imagery, one display each
engine.imagery gains what these rasters need, on every engine:
- geographic (EPSG:4326) tile schemes and a transform applied to each
  decoded tile (the satellite infrared mask);
- staged source changes, which keep the drawn source until the new one
  has loaded (the weather frame swap), and credit-only changes in place;
- raised layers that draw on a shell above terrain and 3D tiles, rising
  with the camera, with a detail image inside a rectangle. On Cesium this
  is the weather shell primitive, moved into the engine; on Navara the
  pixel's ray meets the ellipsoid grown by the height.

Rain radar, satellite clouds, lightning, the wind field and the Bhote
Koshi event and locator each draw through one render/display.js on both
engines; their Navara renderers and Navara's weather surface pass go.
Cesium's pixels are unchanged in every compared scene. On Navara the
Bhote Koshi images are no longer upside down. Two engine-specific parts
stay behind one backend option each: the wind's streamlines and the
Bhote Koshi story entities.

The coupling report now counts defineGroundEffect calls, not its own
declaration. Coupling: Navara renderer lines fall from 2304 to 1695,
Navara ground passes from 3 to 1 (the Global Context pulses), engine
kind checks from 5 to 2. The contract suite passes on both engines
(Cesium 71/71, Navara 74/74).
2026-11-07 11:14:50 +04:00

235 lines
7.5 KiB
JavaScript

#!/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: 'engineExtensionReads',
label: 'Reads of engine.extensions (cesium, navara) outside src/globe',
measure: (files) =>
files
.filter(({ file }) => !inGlobe(file))
.reduce(
(sum, { text }) =>
sum + countMatches(text, /\bextensions\??\.(?:navara|cesium)\b/),
0,
),
},
{
id: 'groundEffectPasses',
label: 'Navara ground passes (defineGroundEffect calls)',
measure: (files) =>
files.reduce(
(sum, { text }) =>
// Calls only: the helper's own declaration is not a pass.
sum + countMatches(text, /(?<!function )\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));