Merge remote-tracking branch 'upstream/main' into fix/363-geocode-proxy

# Conflicts:
#	CHANGELOG.md
This commit is contained in:
Niccolò Saggese committed 2026-09-29 19:16:15 +02:00
commit 5083a74413
542 files changed
+96845 -3034

No files matched your search

+40 -1
View File
@@ -1,3 +1,5 @@
import { HUD_LAYOUTS } from '../hudLayoutPolicy.js';
// Canonical action arguments. Descriptive wording is supplied separately.
const schemas = [
{
@@ -137,7 +139,9 @@ const schemas = [
'local-dams',
'telegeography-submarine-cables',
'local-firms',
'fire-perimeters',
'alpr-cameras',
'local-adsb',
],
},
enabled: {
@@ -169,6 +173,7 @@ const schemas = [
'local-dams',
'telegeography-submarine-cables',
'local-firms',
'fire-perimeters',
'alpr-cameras',
],
},
@@ -310,11 +315,26 @@ const schemas = [
},
layout: {
type: 'string',
enum: ['tactical', 'operator', 'minimal'],
enum: [...HUD_LAYOUTS],
},
},
},
},
{
name: 'set_cyber_sonar',
parameters: {
type: 'object',
additionalProperties: false,
properties: {
enabled: { type: 'boolean' },
rings: { type: 'integer', minimum: 3, maximum: 12 },
rangePct: { type: 'integer', minimum: 60, maximum: 120 },
intensityPct: { type: 'integer', minimum: 0, maximum: 100 },
opacityPct: { type: 'integer', minimum: 35, maximum: 100 },
sectorDeg: { type: 'integer', minimum: 8, maximum: 60 },
},
},
},
{
name: 'set_detection',
parameters: {
@@ -771,6 +791,10 @@ const schemas = [
'ais-live-vessels',
'local-firms',
'earthquakes',
'satellites',
'local-datacenters',
'local-dams',
'fire-perimeters',
],
},
},
@@ -860,6 +884,21 @@ const schemas = [
},
},
},
{
name: 'next_satellite_pass',
parameters: {
type: 'object',
additionalProperties: false,
required: ['target'],
properties: {
target: { type: 'string', minLength: 1, maxLength: 120 },
latitude: { type: 'number', minimum: -90, maximum: 90 },
longitude: { type: 'number', minimum: -180, maximum: 180 },
minElevationDeg: { type: 'number', minimum: 5, maximum: 60 },
visibleOnly: { type: 'boolean' },
},
},
},
];
function freeze(value) {
+48 -3
View File
@@ -15,13 +15,21 @@ const stable = (value) =>
)
: value;
test('the complete Realtime tool payload retains its pre-extraction contract and wording', () => {
test('the complete Realtime tool payload pins the additive analyst, satellite, Local ADS-B and Cyber release', () => {
const digest = createHash('sha256')
.update(JSON.stringify(stable(GEV_REALTIME_TOOLS)))
.update(
JSON.stringify(
stable(
GEV_REALTIME_TOOLS.filter((tool) => tool.name !== 'set_cyber_sonar'),
),
),
)
.digest('hex');
assert.equal(
digest,
'956381c3456d3644ed7c9cda72910dc68a34d9191e0b3e414ee200c348245214',
// Re-derived for the additive `local-adsb` set_layer_visibility value and
// the Cyber HUD layout; the separate sonar tool is excluded above.
'590d537d93e132ac64ac5e211ad5bb9d7d1b1f22e2dd963dda5465fab4510a3b',
);
});
@@ -76,3 +84,40 @@ test('metadata cannot add tools, fields, types or enum values', () => {
])
assert.throws(() => createActionTools(descriptions), TypeError);
});
test('all legacy action arguments are byte-identical after removing the deliberate additions', () => {
const legacy = structuredClone(GEV_ACTION_SCHEMAS).filter(
(tool) => !['next_satellite_pass', 'set_cyber_sonar'].includes(tool.name),
);
const layers = legacy.find((tool) => tool.name === 'analyst_query').parameters
.properties.layers.items;
layers.enum = layers.enum.filter(
(key) =>
![
'satellites',
'local-datacenters',
'local-dams',
'fire-perimeters',
].includes(key),
);
// Local ADS-B is an additive set_layer_visibility enum value.
const visibility = legacy.find((tool) => tool.name === 'set_layer_visibility')
.parameters.properties.layerId;
visibility.enum = visibility.enum.filter(
(key) => !['local-adsb', 'fire-perimeters'].includes(key),
);
for (const tool of legacy) {
for (const value of Object.values(tool.parameters.properties)) {
if (value.enum)
value.enum = value.enum.filter((key) => key !== 'fire-perimeters');
}
}
// Independently derived by executing trusted c9f9896 actionSchemas in the restricted container.
const hud = legacy.find((tool) => tool.name === 'set_hud').parameters
.properties.layout;
hud.enum = hud.enum.filter((layout) => layout !== 'cyber');
assert.equal(
createHash('sha256').update(JSON.stringify(legacy)).digest('hex'),
'820fff21658f6907e1010b2b79c5431a77f4e34afd2277d62d8de46c368b6f8c',
);
});
+137 -1
View File
@@ -1,3 +1,8 @@
import {
layerSnapshot,
layerSnapshots,
feedProvenanceEnvelope,
} from '../data/layerSnapshot.js';
import { readLayerLifecycleSummary } from './layerSummary.js';
export { readLayerLifecycleSummary } from './layerSummary.js';
import { defaultGeospatial } from '../search/defaults.js';
@@ -212,6 +217,9 @@ const LAYER_ALIASES = new Map([
['submarine cables', 'telegeography-submarine-cables'],
['cables', 'telegeography-submarine-cables'],
['telegeography', 'telegeography-submarine-cables'],
['fire perimeters', 'fire-perimeters'],
['perimeters', 'fire-perimeters'],
['wildfire perimeters', 'fire-perimeters'],
['firms', 'local-firms'],
['fires', 'local-firms'],
['active fires', 'local-firms'],
@@ -221,6 +229,12 @@ const LAYER_ALIASES = new Map([
['license plate readers', 'alpr-cameras'],
['license plate cameras', 'alpr-cameras'],
['plate readers', 'alpr-cameras'],
['local-adsb', 'local-adsb'],
['local adsb', 'local-adsb'],
['local ads-b', 'local-adsb'],
['my receiver', 'local-adsb'],
['my antenna', 'local-adsb'],
['my sdr', 'local-adsb'],
]);
const CITY_ALIASES = new Map([
@@ -964,6 +978,10 @@ export function createGevActionRunner({
};
}
if (name === 'next_satellite_pass') {
return nextSatellitePass(viewer, dataManager, args);
}
if (name === 'next_iss_pass') {
return nextIssPass(viewer, dataManager, args);
}
@@ -1040,6 +1058,17 @@ export function createGevActionRunner({
return { ...out, hud: styleManager.getControlState().hud };
}
if (name === 'set_cyber_sonar') {
if (typeof styleManager?.setCyberSonar !== 'function') {
return {
ok: false,
action: 'set_cyber_sonar',
error: 'Cyber sonar controls are unavailable.',
};
}
return { action: 'set_cyber_sonar', ...styleManager.setCyberSonar(args) };
}
if (name === 'set_detection') {
const result = styleManager.setDetection({
enabled: typeof args.enabled === 'boolean' ? args.enabled : undefined,
@@ -2655,6 +2684,64 @@ function nextIssPass(viewer, dataManager, args) {
durationMin: Math.max(1, Math.round((pass.setMs - pass.riseMs) / 60000)),
peakElevationDeg: Math.round(pass.maxElevDeg),
riseDirection: compassDir(pass.riseAzDeg),
visible: typeof pass.visible === 'boolean' ? pass.visible : null,
visibilityNote:
'Geometric illumination estimate only; weather, brightness and orbital-element age affect actual visibility.',
setIso: new Date(pass.setMs).toISOString(),
peakIso: new Date(pass.maxElevMs).toISOString(),
};
}
function nextSatellitePass(viewer, dataManager, args) {
const layer = dataManager?.layers?.get('satellites')?.module;
const identity = layer?.resolveSatelliteForPass?.(args.target) || {
status: 'not-found',
};
if (identity.status !== 'ok')
return {
ok: false,
action: 'next_satellite_pass',
...identity,
error:
identity.status === 'ambiguous'
? 'Several loaded satellites match. Choose a NORAD ID from candidates.'
: 'No loaded satellite matches. Enable satellites and use an exact name or NORAD ID.',
};
// Reuse the legacy location fallback and result formatting, substituting only
// this explicitly resolved catalog identity and the optional visibility filter.
const adapter = {
layers: new Map([
[
'satellites',
{
module: {
getNextIssPass: (options) =>
layer.getNextSatellitePass(identity.noradId, {
...options,
requireVisible: args.visibleOnly === true,
}),
},
},
],
]),
};
const result = nextIssPass(viewer, adapter, args);
if (result.error) {
result.error = result.error.replace(
/ISS/g,
identity.name || String(identity.noradId),
);
if (args.visibleOnly === true)
result.error +=
' Search required estimated illumination under a dark sky.';
}
return {
...result,
action: 'next_satellite_pass',
noradId: identity.noradId,
name: identity.name,
visibleOnly: args.visibleOnly === true,
horizonHours: 24,
};
}
@@ -3025,7 +3112,13 @@ function getCurrentViewState(
enabled: layer.enabled,
count: layer.stats?.count || 0,
error: layer.stats?.error || null,
feedState: layerSnapshot(layer).feedState,
source: layerSnapshot(layer).source,
lastUpdate: layerSnapshot(layer).lastUpdate,
})),
feedProvenance: feedProvenanceEnvelope(
layerSnapshots(dataManager.getAll()).filter((layer) => layer.enabled),
),
};
}
@@ -4152,6 +4245,29 @@ function analystProviders(
? mod.getAnalystRecords(requestedLimit) || []
: mod.getAnalystRecords() || [];
},
getLayerSnapshot(layerKey) {
const row = dataManager.getAll?.().find((layer) => layer.id === layerKey);
if (row) return layerSnapshot(row);
const module = dataManager.layers?.get(layerKey)?.module;
return layerSnapshot({
id: layerKey,
enabled: dataManager.isEnabled?.(layerKey),
stats: module?.getStats?.() || {},
});
},
getRecordCoverage(layerKey, rows) {
if (!['satellites', 'local-datacenters', 'local-dams'].includes(layerKey))
return null;
const module = dataManager.layers.get(layerKey)?.module;
const loaded = module?.getStats?.().count;
return {
basis: 'bounded-loaded-records',
recordsExamined: rows.length,
loadedCount: Number.isFinite(loaded) ? loaded : null,
sourceTruncated: Number.isFinite(loaded) ? loaded > rows.length : null,
note: 'Counts and ranks apply only to these examined loaded records, not all satellites or infrastructure; distance is ground great-circle distance.',
};
},
resolveRegionRing,
/**
* The active Contacts subject, when there is one — the centre the operator
@@ -4207,6 +4323,7 @@ async function runAnalystQuery(
return {
ok: false,
action: 'analyst_query',
...(result.code ? { code: result.code } : {}),
error: result.error,
coverage: result.coverage,
};
@@ -4243,6 +4360,12 @@ async function runAnalystQuery(
'distanceKm',
'confidence',
'place',
'noradId',
'satelliteClass',
'group',
'river',
'output',
'capacity',
]) {
if (r[k] !== null && r[k] !== undefined) compact[k] = r[k];
}
@@ -4282,7 +4405,18 @@ async function runAnalystQuery(
args,
result,
);
if (entityWindow) return entityWindow;
if (entityWindow) {
const provenance = feedProvenanceEnvelope(
layerSnapshots(dataManager.getAll?.() || []).filter(
(layer) => layer.enabled && ['flights', 'military'].includes(layer.id),
),
);
return {
...entityWindow,
feedProvenance: provenance,
feedState: provenance.overall,
};
}
const contactsWindow = activeContactsWindow(dataManager);
const aircraftQueried = (result.coverage?.layersQueried || []).some(
@@ -4321,6 +4455,8 @@ async function runAnalystQuery(
items,
summary: result.summary,
coverage: result.coverage,
feedProvenance: result.coverage?.feedProvenance || null,
feedState: result.coverage?.feedProvenance?.overall || null,
// The panel's own numbers, carried so the answer can match what the
// operator is looking at regardless of how the model reads the note.
// Flattened alongside the object so the count and its subject cannot be
+112
View File
@@ -3165,6 +3165,30 @@ test('ALPR common names toggle only the registered camera layer through the norm
}
});
test('Local ADS-B common names toggle only the receiver layer through the normal voice action', async () => {
globalThis.window = globalThis.window || { clearTimeout, setTimeout, requestIdleCallback: null };
const viewer = { clock: { onTick: { addEventListener: () => () => {} } },
scene: { canvas: { addEventListener() {}, removeEventListener() {} } },
camera: { moveEnd: { addEventListener() {} } } };
const calls = [];
let enabled = false;
const dataManager = {
layers: new Map([['local-adsb', { module: {} }]]),
getAll: () => [{ id: 'local-adsb', name: 'Local ADS-B' }],
isEnabled: () => enabled,
setEnabled: async (id, value) => { calls.push([id, value]); enabled = value; return true; },
};
const runner = createGevActionRunner({ viewer, styleManager: {}, dataManager });
for (const alias of ['local-adsb', 'local ADS-B', 'Local ADSB', 'my receiver', 'my antenna', 'my SDR']) {
for (const value of [true, false]) {
const result = await runner('set_layer_visibility', { layerId: alias, enabled: value });
assert.equal(result.ok, true);
assert.equal(result.layerId, 'local-adsb');
assert.deepEqual(calls.at(-1), ['local-adsb', value]);
}
}
});
test('ISS voice lookup uses the registered satellite instance', async () => {
const calls = [];
const viewer = {
@@ -3182,3 +3206,91 @@ test('ISS voice lookup uses the registered satellite instance', async () => {
assert.deepEqual(calls, [{ latDeg: 30, lonDeg: -97, minElevDeg: 15 }]);
assert.match(result.error, /No ISS pass above 15/);
});
test('analyst_query and get_current_view_state carry stale feed provenance', async () => {
globalThis.window = globalThis.window || { clearTimeout, setTimeout, requestIdleCallback: null };
const now = Date.now();
const flights = {
id: 'flights',
source: 'OpenSky Network',
getStats: () => ({
source: 'OpenSky Network',
stale: true,
count: 12,
lastUpdate: now - 240_000,
}),
getAnalystRecords: () => ([
{ id: 'SWA1', icao24: 'aaa001', lat: 30.27, lon: -97.74, altitudeM: 11000, onGround: false },
]),
};
const viewer = {
clock: { onTick: { addEventListener: () => () => {} } },
scene: { canvas: { addEventListener() {}, removeEventListener() {} } },
camera: {
moveEnd: { addEventListener() {} },
positionWC: Cesium.Cartesian3.fromDegrees(-97.7, 30.2, 1000),
positionCartographic: { height: 300_000, latitude: 0.52, longitude: -1.71 },
},
};
const dataManager = {
layers: new Map([['flights', { module: flights }]]),
isEnabled: (id) => id === 'flights',
getAll: () => [{
id: 'flights',
name: 'Live Flights',
enabled: true,
source: 'OpenSky Network',
stats: flights.getStats(),
}],
};
const runner = createGevActionRunner({
viewer,
styleManager: {
activeStyle: 'normal',
getContextModeState: () => ({ mode: null, active: false }),
getCockpitState: () => ({ active: false }),
getControlState: () => null,
},
dataManager,
});
const view = await runner('get_current_view_state');
assert.equal(view.layers[0].feedState, 'stale');
assert.equal(view.feedProvenance.overall, 'stale');
assert.match(view.feedProvenance.note, /STALE/);
const query = await runner('analyst_query', {
layers: ['flights'],
scope: { kind: 'view' },
limit: 5,
});
assert.equal(query.ok, true);
assert.equal(query.feedState, 'stale');
assert.equal(query.feedProvenance.overall, 'stale');
assert.equal(query.coverage.layersQueried[0].feedState, 'stale');
assert.match(query.feedProvenance.note, /do not describe this as live/i);
});
test('general satellite pass resolves once, refuses ambiguity, and preserves ISS call semantics', async () => {
const calls = [];
const pass = { riseMs: Date.now() + 60000, setMs: Date.now() + 360000, maxElevMs: Date.now() + 180000, maxElevDeg: 30, riseAzDeg: 90, visible: false };
const layer = {
resolveSatelliteForPass(target) { return target === 'starlink' ? { status: 'ambiguous', candidates: [{ noradId: 1 }, { noradId: 2 }] } : { status: 'ok', noradId: 25544, name: 'ISS' }; },
getNextSatellitePass(id, options) { calls.push({ id, ...options }); return { status: 'ok', pass }; },
getNextIssPass(options) { calls.push(options); return { status: 'ok', pass }; },
};
const viewer = { clock: { onTick: { addEventListener: () => () => {} } }, scene: { canvas: { addEventListener() {}, removeEventListener() {} } }, camera: { moveEnd: { addEventListener() {} } } };
const runner = createGevActionRunner({ viewer, styleManager: {}, dataManager: { layers: new Map([['satellites', { module: layer }]]) } });
const ambiguous = await runner('next_satellite_pass', { target: 'starlink' });
assert.equal(ambiguous.status, 'ambiguous');
assert.equal(calls.length, 0);
const args = { latitude: 30, longitude: -97, minElevationDeg: 15 };
const general = await runner('next_satellite_pass', { target: '25544', visibleOnly: true, ...args });
assert.equal(general.action, 'next_satellite_pass');
assert.deepEqual(calls[0], { id: 25544, latDeg: 30, lonDeg: -97, minElevDeg: 15, requireVisible: true });
const iss = await runner('next_iss_pass', args);
assert.deepEqual(calls[1], { latDeg: 30, lonDeg: -97, minElevDeg: 15 });
for (const key of ['observer', 'riseIso', 'minutesFromNow', 'durationMin', 'peakElevationDeg', 'riseDirection']) assert.deepEqual(iss[key], general[key]);
assert.equal(iss.visible, false);
assert.equal(iss.action, 'next_iss_pass');
});