mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-15 15:52:21 +00:00
Task 12a. The Local ADS-B logic (index.js, models.js) no longer imports
Cesium. src/layers/localAdsb/render/cesium.js holds what it built, one
for one: the CustomDataSource of marker entities (billboard with
per-frame position/rotation/visibility CallbackProperties, the 2,000 km
DistanceDisplayCondition, the 978 MHz UAT ring as a PointGraphics), the
model PrimitiveCollection (models load, pose and leave through the shared
aircraft renderer's model part, adopting that collection), the selected
aircraft's trail (data/trailRenderer, as before) and live head segment,
and the camera reads. Registered for 'local-adsb' in the renderer
registry, created at init(viewer, {engine}).
- Clicks go through the engine's selection dispatcher (drill 8) instead of
the layer's ScreenSpaceEventHandler, with the layer's own rule kept as is
— a new `lookUnder` owner kind: its marker on top, or under picks no
layer owns and under trail lines, is selected (and re-selected);
another layer's contact on top, or empty map, releases only its own
selection; a trail line alone does nothing; no gesture rule. To the
other owners it is another layer, as its pick-owner registration made
it before. (The dispatcher's `drillThrough` would have taken the marker
from under another layer's contact, which the old handler never did.)
- models.js reads the camera through the renderer's view (frustum and
horizon tests, ground snap owner) and draws through renderer.models
(loadFleet/add/remove/destroy/setPose: the same MIX-tinted glTF options,
the same HPR frame with the Flights nose offset). The shared aircraft
renderer gains a loadModel seam (default Cesium.Model.fromGltfAsync).
- Cesium math → src/geo; the model tint is an Rgba equal to the old
Cesium.Color; check-import-directions: src/layers/localAdsb/ joins the
rendered layers.
Tests moved with the code, same meaning:
- index.test: the services fixture creates the layer's Cesium renderer and
the selection dispatcher; the click harness scripts the picks on a fake
engine registered for the fake viewer (was a fake ScreenSpaceEventHandler)
and the app's pick owners (trails, a Flights contact) are registered in
the pick registry (was an injected picking service).
- cockpitModels.test: the GLB loads are the aircraft renderer's loadModel
calls (Cesium.Model.fromGltfAsync by default), each asynchronous:false.
- trackingClickGesture.test: the dispatcher's empty click goes to the
plain owners (filter(plain)).
- renderers.test: local-adsb has an extracted Cesium renderer.
New: the dispatcher's look-under rule beside a plain owner (7 click cases).
W1 replay (flights, military, localAdsb) vs a fresh 7c7422e2 run: counts,
known objects, screen points, tracking poses, requests and every shot's
sha256 identical.
376 lines
14 KiB
JavaScript
376 lines
14 KiB
JavaScript
import { execFileSync } from 'node:child_process';
|
|
import { existsSync, readFileSync, realpathSync } from 'node:fs';
|
|
import { builtinModules } from 'node:module';
|
|
import path from 'node:path';
|
|
import { fileURLToPath } from 'node:url';
|
|
import { analyzeModule } from './module-analysis.mjs';
|
|
|
|
const builtins = new Set(
|
|
builtinModules.flatMap((name) => [name, `node:${name}`]),
|
|
);
|
|
const code = /\.[mc]?js$/;
|
|
const entry = (file) =>
|
|
file === 'src/main.js' || file.startsWith('src/standalone/');
|
|
const tests = (file) =>
|
|
file.endsWith('.test.mjs') ||
|
|
file.startsWith('src/testSupport/') ||
|
|
file.startsWith('src/tooling/') ||
|
|
file === 'src/overlays/worldOverlayAllocation.worker.mjs';
|
|
const source = (file) =>
|
|
file.startsWith('src/sources/') ||
|
|
/^src\/layers\/[^/]+\/(?:source|bundledSource|flowSource)\.js$/.test(file);
|
|
// Engine-neutral geometry ported from Cesium; every runtime file is a portable root.
|
|
const geo = (file) => file.startsWith('src/geo/');
|
|
// The app clock (src/time): plain numbers, no engine, no DOM.
|
|
const time = (file) => file.startsWith('src/time/');
|
|
// The engine-neutral globe contract and its test support; engines live below it.
|
|
const globeContract = (file) =>
|
|
/^src\/globe\/[^/]+$/.test(file) || file.startsWith('src/globe/testing/');
|
|
const globeEngine = (file) => /^src\/globe\/(?:cesium|navara)\//.test(file);
|
|
const cesiumPackage = /^(?:cesium(?:\/|$)|@cesium\/)/;
|
|
// Navara renders through three.js and its postprocessing peer.
|
|
const navaraPackage = /^(?:(?:three|postprocessing)(?:\/|$)|@navaramap\/)/;
|
|
// Phase 3: the layers moved onto layer renderers (the *a tasks) keep each
|
|
// engine in their renderers: cesium only in render/cesium*.js, three and
|
|
// Navara only in render/navara*.js. The list grows with each moved layer.
|
|
const RENDERED_LAYER_DIRS = [
|
|
'src/layers/flights/',
|
|
'src/layers/military/',
|
|
'src/layers/localAdsb/',
|
|
'src/layers/aircraft/',
|
|
'src/layers/shared/',
|
|
];
|
|
const renderedLayer = (file) =>
|
|
RENDERED_LAYER_DIRS.some((dir) => file.startsWith(dir));
|
|
const cesiumLayerRenderer = (file) => /\/render\/cesium[^/]*\.js$/.test(file);
|
|
const navaraLayerRenderer = (file) => /\/render\/navara[^/]*\.js$/.test(file);
|
|
const renderer = (file) =>
|
|
/^src\/(?:ui|app|standalone|overlays)\//.test(file) ||
|
|
/^src\/layers\/[^/]+\/(?:index|rendering|snapshotRenderer|overlay|presentation|cards|controls)\.js$/.test(
|
|
file,
|
|
);
|
|
// Two compatibility composition entries intentionally select standalone defaults.
|
|
const compatibilityEdge = (from, to) =>
|
|
(from === 'server/providers/local.js' &&
|
|
to === 'server/standalone/key-setup.js') ||
|
|
(from === 'src/ui.js' && to === 'src/standalone/catalog.js');
|
|
const portableExport = (key) =>
|
|
key.startsWith('./sources/') ||
|
|
/\/source$/.test(key) ||
|
|
/^\.\/layers\/(?:flights|military|vessels)\/(?:records|ingestion)$/.test(
|
|
key,
|
|
) ||
|
|
[
|
|
'./director',
|
|
'./tools',
|
|
'./tools/mcp',
|
|
'./tools/services',
|
|
'./view',
|
|
'./voice/action-schemas',
|
|
'./voice/session',
|
|
'./data/lifecycle',
|
|
].includes(key);
|
|
|
|
/** Check every runtime file, plus transitive portable/source graphs, independently of bundler reachability. */
|
|
export function checkImportDirections(root) {
|
|
root = realpathSync(root);
|
|
const pkg = JSON.parse(readFileSync(path.join(root, 'package.json'), 'utf8'));
|
|
const files = [
|
|
...new Set(
|
|
execFileSync(
|
|
'git',
|
|
[
|
|
'ls-files',
|
|
'-z',
|
|
'--cached',
|
|
'--others',
|
|
'--exclude-standard',
|
|
'--',
|
|
'src',
|
|
'server',
|
|
],
|
|
{ cwd: root, encoding: 'utf8' },
|
|
).split('\0'),
|
|
),
|
|
].filter(
|
|
(file) =>
|
|
code.test(file) && !tests(file) && existsSync(path.join(root, file)),
|
|
);
|
|
const errors = new Set();
|
|
const modules = new Map();
|
|
const relative = (file) =>
|
|
path.relative(root, file).split(path.sep).join('/');
|
|
const report = (file, message) => errors.add(`${file}: ${message}`);
|
|
function load(file) {
|
|
if (modules.has(file)) return modules.get(file);
|
|
const resolved = realpathSync(path.join(root, file));
|
|
if (relative(resolved) !== file) {
|
|
report(
|
|
file,
|
|
'Runtime modules must not alias other owners through symlinks',
|
|
);
|
|
return null;
|
|
}
|
|
let analysis;
|
|
try {
|
|
analysis = analyzeModule(readFileSync(resolved, 'utf8'), {
|
|
dynamic: true,
|
|
});
|
|
} catch (error) {
|
|
report(file, error.message);
|
|
return null;
|
|
}
|
|
const record = { ...analysis, edges: [], external: [] };
|
|
modules.set(file, record);
|
|
for (const specifier of analysis.imports) {
|
|
let target;
|
|
if (specifier.startsWith('.'))
|
|
target = path.resolve(path.dirname(resolved), specifier.split('?')[0]);
|
|
else if (specifier === pkg.name || specifier.startsWith(`${pkg.name}/`)) {
|
|
const key =
|
|
specifier === pkg.name ? '.' : '.' + specifier.slice(pkg.name.length);
|
|
const exported = pkg.exports?.[key];
|
|
if (typeof exported !== 'string') {
|
|
report(file, `Unsupported self import: ${specifier}`);
|
|
continue;
|
|
}
|
|
target = path.resolve(root, exported);
|
|
} else {
|
|
record.external.push(specifier);
|
|
if (renderedLayer(file) && !tests(file)) {
|
|
if (cesiumPackage.test(specifier) && !cesiumLayerRenderer(file))
|
|
report(
|
|
file,
|
|
`cesium only in the layer's render/cesium*.js: ${specifier}`,
|
|
);
|
|
if (navaraPackage.test(specifier) && !navaraLayerRenderer(file))
|
|
report(
|
|
file,
|
|
`three and Navara only in the layer's render/navara*.js: ${specifier}`,
|
|
);
|
|
}
|
|
if (
|
|
file.startsWith('src/') &&
|
|
(builtins.has(specifier) || specifier.startsWith('node:'))
|
|
)
|
|
report(file, `Node builtin in browser module: ${specifier}`);
|
|
if (
|
|
specifier.startsWith('/') ||
|
|
(/^[a-z]+:/i.test(specifier) && !specifier.startsWith('node:'))
|
|
)
|
|
report(file, `Use repository or package imports: ${specifier}`);
|
|
continue;
|
|
}
|
|
if (!existsSync(target)) {
|
|
report(file, `Missing import: ${specifier}`);
|
|
continue;
|
|
}
|
|
const to = relative(realpathSync(target));
|
|
if (to.startsWith('../') || path.isAbsolute(to)) {
|
|
report(file, `Import escapes repository: ${specifier}`);
|
|
continue;
|
|
}
|
|
record.edges.push(to);
|
|
// Navara stays out of the Cesium-mode bundle: outside its own directory
|
|
// it is reached only through import() (src/app/scene.js), which the
|
|
// bundler splits into a chunk loaded on ?engine=navara.
|
|
if (
|
|
to.startsWith('src/globe/navara/') &&
|
|
!file.startsWith('src/globe/navara/') &&
|
|
!analysis.dynamicImports.includes(specifier)
|
|
)
|
|
report(
|
|
file,
|
|
`The Navara engine loads only through a dynamic import(): ${to}`,
|
|
);
|
|
// Like the engine, a layer's Navara renderer reaches the app only
|
|
// through the Navara mode's import() (src/app/navaraMode.js).
|
|
if (
|
|
navaraLayerRenderer(to) &&
|
|
!navaraLayerRenderer(file) &&
|
|
!(
|
|
file === 'src/app/navaraMode.js' &&
|
|
analysis.dynamicImports.includes(specifier)
|
|
)
|
|
)
|
|
report(
|
|
file,
|
|
`A Navara layer renderer loads only through the Navara mode's dynamic import(): ${to}`,
|
|
);
|
|
if (file.startsWith('src/') && to.startsWith('server/'))
|
|
report(file, `Browser imports server: ${to}`);
|
|
if (
|
|
!compatibilityEdge(file, to) &&
|
|
file.startsWith('server/providers/') &&
|
|
(to.startsWith('server/standalone/') || renderer(to))
|
|
)
|
|
report(file, `Provider imports application/rendering: ${to}`);
|
|
if (
|
|
!compatibilityEdge(file, to) &&
|
|
file.startsWith('src/') &&
|
|
!entry(file) &&
|
|
entry(to)
|
|
)
|
|
report(file, `Reusable module imports standalone setup: ${to}`);
|
|
if (source(file) && renderer(to))
|
|
report(file, `Source imports rendering/application: ${to}`);
|
|
if (file === 'src/voice/gevActions.js' && to === 'src/data/manager.js')
|
|
report(
|
|
file,
|
|
'Actions must consume feed state without the manager facade',
|
|
);
|
|
if (
|
|
file === 'src/app/constructCatalog.js' &&
|
|
to === 'src/data/localGeojson.js'
|
|
)
|
|
report(
|
|
file,
|
|
'Catalog must use the services owner without compatibility layer construction',
|
|
);
|
|
}
|
|
return record;
|
|
}
|
|
for (const file of files) load(file);
|
|
// A helper outside src must not smuggle Node or server code into a browser graph.
|
|
const browserSeen = new Set();
|
|
function browser(file) {
|
|
if (browserSeen.has(file) || !code.test(file)) return;
|
|
browserSeen.add(file);
|
|
const info = load(file);
|
|
if (!info) return;
|
|
for (const specifier of info.external) {
|
|
if (builtins.has(specifier) || specifier.startsWith('node:'))
|
|
report(file, `Browser graph reaches Node builtin: ${specifier}`);
|
|
}
|
|
for (const to of info.edges) {
|
|
if (to.startsWith('server/') || tests(to))
|
|
report(file, `Browser graph reaches non-runtime owner: ${to}`);
|
|
else browser(to);
|
|
}
|
|
}
|
|
for (const file of files.filter((file) => file.startsWith('src/')))
|
|
browser(file);
|
|
const roots = new Map(files.filter(source).map((file) => [file, 'source']));
|
|
for (const file of files.filter(geo)) roots.set(file, 'geo');
|
|
for (const file of files.filter(time)) roots.set(file, 'time');
|
|
for (const [key, value] of Object.entries(pkg.exports || {})) {
|
|
if (portableExport(key) && typeof value === 'string')
|
|
roots.set(value.replace(/^\.\//, ''), key);
|
|
}
|
|
roots.set('src/data/feedState.js', 'feed state');
|
|
for (const [start, label] of roots) {
|
|
if (!existsSync(path.join(root, start))) {
|
|
report(start, `Missing portable entry: ${label}`);
|
|
continue;
|
|
}
|
|
const seen = new Set();
|
|
function visit(file) {
|
|
if (seen.has(file) || !code.test(file)) return;
|
|
seen.add(file);
|
|
const info = load(file);
|
|
if (!info) return;
|
|
if (info.browser.length)
|
|
report(
|
|
file,
|
|
`${label} reaches browser globals: ${info.browser.join(', ')}`,
|
|
);
|
|
for (const specifier of info.external) {
|
|
if (
|
|
builtins.has(specifier) ||
|
|
specifier.startsWith('node:') ||
|
|
/^(?:cesium|vite)(?:\/|$)/.test(specifier) ||
|
|
// Geo must not reach Cesium through its workspace packages either.
|
|
(label === 'geo' && /^@cesium\//.test(specifier)) ||
|
|
// Neither engine reaches the app clock.
|
|
(label === 'time' &&
|
|
(cesiumPackage.test(specifier) || navaraPackage.test(specifier)))
|
|
)
|
|
report(
|
|
file,
|
|
`${label} reaches platform/rendering dependency: ${specifier}`,
|
|
);
|
|
}
|
|
for (const to of info.edges) {
|
|
if (renderer(to) || to.startsWith('server/'))
|
|
report(file, `${label} reaches application/rendering: ${to}`);
|
|
else visit(to);
|
|
}
|
|
}
|
|
visit(start);
|
|
}
|
|
// The globe contract reaches no engine, directly or through helpers; each
|
|
// engine implementation imports only its own engine's packages.
|
|
const globeSeen = new Set();
|
|
function globe(file) {
|
|
if (globeSeen.has(file) || !code.test(file)) return;
|
|
globeSeen.add(file);
|
|
const info = load(file);
|
|
if (!info) return;
|
|
for (const specifier of info.external) {
|
|
if (cesiumPackage.test(specifier) || navaraPackage.test(specifier))
|
|
report(file, `globe contract reaches engine dependency: ${specifier}`);
|
|
}
|
|
for (const to of info.edges) {
|
|
if (globeEngine(to))
|
|
report(file, `globe contract imports engine implementation: ${to}`);
|
|
else globe(to);
|
|
}
|
|
}
|
|
for (const file of files.filter(globeContract)) globe(file);
|
|
// Each engine graph stays on its own engine, through helpers too.
|
|
for (const start of files.filter(globeEngine)) {
|
|
const navara = start.startsWith('src/globe/navara/');
|
|
const label = navara ? 'Navara globe engine' : 'Cesium globe engine';
|
|
const foreignPackage = navara ? cesiumPackage : navaraPackage;
|
|
const foreignEngine = navara ? 'src/globe/cesium/' : 'src/globe/navara/';
|
|
const seen = new Set();
|
|
const visit = (file) => {
|
|
if (seen.has(file) || !code.test(file)) return;
|
|
seen.add(file);
|
|
const info = load(file);
|
|
if (!info) return;
|
|
for (const specifier of info.external)
|
|
if (foreignPackage.test(specifier))
|
|
report(file, `${label} reaches ${specifier}`);
|
|
for (const to of info.edges) {
|
|
if (to.startsWith(foreignEngine))
|
|
report(file, `${label} reaches ${to}`);
|
|
else visit(to);
|
|
}
|
|
};
|
|
visit(start);
|
|
}
|
|
// Common voice controls may use their DOM view, but never a protocol implementation.
|
|
const voiceSeen = new Set();
|
|
function voice(file) {
|
|
if (voiceSeen.has(file)) return;
|
|
voiceSeen.add(file);
|
|
const info = load(file);
|
|
for (const to of info?.edges || []) {
|
|
if (/^src\/voice\/realtime/.test(to))
|
|
report(file, `Common voice controls import protocol: ${to}`);
|
|
else if (code.test(to)) voice(to);
|
|
}
|
|
}
|
|
if (existsSync(path.join(root, 'src/voice/sessionCommands.js')))
|
|
voice('src/voice/sessionCommands.js');
|
|
if (errors.size) throw new Error([...errors].join('\n'));
|
|
return { modules: files.length, portableEntries: roots.size };
|
|
}
|
|
|
|
if (
|
|
process.argv[1] &&
|
|
path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)
|
|
) {
|
|
try {
|
|
console.log(
|
|
JSON.stringify(
|
|
checkImportDirections(fileURLToPath(new URL('../', import.meta.url))),
|
|
),
|
|
);
|
|
} catch (error) {
|
|
console.error(error.message);
|
|
process.exitCode = 1;
|
|
}
|
|
}
|