mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-15 15:52:21 +00:00
Navara now matches Cesium where the review found it diverging: a flight holds the camera inputs, camera.changed counts turns as Cesium does, render requests made during a frame survive it, follow pinches zoom, ground widths follow the pixel ratio and Cyber sonar reaches glyph caches and models. On Cesium, a staged imagery swap keeps loading tiles, setView replaces a running flight, and a cleared weather detail goes away. The contract gains MoverSet.failed, maps.terrainRevision, render.maxTextureSize and surface.sampleTerrainMostDetailed on every engine, so layers retry failed models, key road heights by terrain, size weather textures to the GPU and place Bhote Koshi cards on detailed terrain. Engine switches keep the live view, follows, focus, layer options and cards: a pick hands over the exact pose, applies no address options and pans no carried-over card; a history visit still applies its entry. Layers put GhostMaps pins, cable landings, OSM Places and directions on the measured surface without per-frame sampling, draw the Cockpit contact dot, hide disabled CCTV on attach, and keep ALPR running outside secure contexts. KML imports draw raised polygon fills, rotated and coloured ground overlays and north-referenced icon headings, and report files with nothing to draw. QA scripts read the contract instead of Cesium internals (test:track is 109/109 again), wait for the camera to rest before clicking, and give cold switches and parked engines realistic time. Docs follow the code.
372 lines
13 KiB
JavaScript
372 lines
13 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|vue)$/;
|
|
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\/)/;
|
|
const renderer = (file) =>
|
|
/^src\/(?:ui|app|standalone|overlays)\//.test(file) ||
|
|
/^src\/layers\/[^/]+\/(?:index|rendering|snapshotRenderer|overlay|presentation|cards|controls)\.js$/.test(
|
|
file,
|
|
);
|
|
// One compatibility composition entry intentionally selects standalone defaults.
|
|
const compatibilityEdge = (from, to) =>
|
|
from === 'server/providers/local.js' &&
|
|
to === 'server/standalone/key-setup.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 boundaryFile = path.join(root, 'scripts/package-boundaries.json');
|
|
const { engineImportExceptions = [] } = existsSync(boundaryFile)
|
|
? JSON.parse(readFileSync(boundaryFile, 'utf8'))
|
|
: {};
|
|
const exceptionKeys = new Set();
|
|
for (const item of engineImportExceptions) {
|
|
const key = `${item.file}:${item.package}`;
|
|
if (
|
|
typeof item.file !== 'string' ||
|
|
!files.includes(item.file) ||
|
|
!(cesiumPackage.test(item.package) || navaraPackage.test(item.package)) ||
|
|
typeof item.reason !== 'string' ||
|
|
!item.reason.trim() ||
|
|
exceptionKeys.has(key)
|
|
)
|
|
throw new Error(`Invalid engine import exception: ${key}`);
|
|
exceptionKeys.add(key);
|
|
}
|
|
const usedExceptions = new Set();
|
|
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,
|
|
filename: file,
|
|
});
|
|
} 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);
|
|
const owner = cesiumPackage.test(specifier)
|
|
? 'cesium'
|
|
: navaraPackage.test(specifier)
|
|
? 'navara'
|
|
: null;
|
|
if (owner && !file.startsWith(`src/globe/${owner}/`)) {
|
|
const exception = engineImportExceptions.find(
|
|
(item) => item.file === file && item.package === specifier,
|
|
);
|
|
if (exception) usedExceptions.add(exception);
|
|
else
|
|
report(file, `${specifier} imports belong in src/globe/${owner}/`);
|
|
}
|
|
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.staticImports.includes(specifier)
|
|
)
|
|
report(
|
|
file,
|
|
`The Navara engine loads only through a 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',
|
|
);
|
|
}
|
|
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 owner of ['cesium', 'navara']) {
|
|
const navara = owner === '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);
|
|
}
|
|
};
|
|
for (const start of files.filter((file) =>
|
|
file.startsWith(`src/globe/${owner}/`),
|
|
))
|
|
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');
|
|
for (const item of engineImportExceptions) {
|
|
if (!usedExceptions.has(item))
|
|
report(item.file, `Unused engine import exception: ${item.package}`);
|
|
}
|
|
if (errors.size) throw new Error([...errors].join('\n'));
|
|
return {
|
|
modules: files.length,
|
|
portableEntries: roots.size,
|
|
engineImportExceptionCount: usedExceptions.size,
|
|
engineImportExceptions: [...usedExceptions],
|
|
};
|
|
}
|
|
|
|
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;
|
|
}
|
|
}
|