mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-05 10:53:59 +00:00
fix: pin additive voice contracts and require HUD provenance wording
This commit is contained in:
1 parent
6d6d43c36c
commit
1e8f919cf3
16 files changed
+368
-154
No files matched your search
@@ -1,4 +1,7 @@
|
||||
import { HUD_SUMMARY_INSTRUCTIONS, keylessHudSummaryResponse } from '../../../src/hudSummaryResponse.js';
|
||||
import {
|
||||
HUD_SUMMARY_INSTRUCTIONS,
|
||||
keylessHudSummaryResponse,
|
||||
} from '../../../src/hudSummaryResponse.js';
|
||||
import { enforceOptInRateLimit, openAiRateLimiter } from './rate-limit.js';
|
||||
import { readRequestBody } from '../common/request.js';
|
||||
import { OPENAI_HUD_SUMMARY_MODEL_DEFAULT } from './constants.js';
|
||||
|
||||
@@ -19,7 +19,7 @@ function realtimeInstructions(
|
||||
'analyst_query ANSWERS questions; it never moves the camera or starts tracking. For requests to FOLLOW or TRACK a specific aircraft/ship, call track_entity (get_entity_context first when the target is ambiguous), never analyst_query as the final or only action. For "follow/track the nearest aircraft", first call analyst_query with the aircraft layer(s), sortBy=distance, and limit=1, then call track_entity with the returned aircraft identity in the same turn. The lookup alone does not fulfill a follow/track command.',
|
||||
'For a request to enable an aircraft layer and SELECT or FIND the nearest/closest aircraft near a named place — for example, "Turn on flights and select the closest aircraft to Austin" — call select_nearest_aircraft once. It atomically turns on the requested aircraft layer first, waits for location arrival, refreshes that layer for the destination viewport, filters out landed/on-ground records, and selects the nearest airborne result. A healthy fallback feed is valid data: report the returned feed source briefly, never call it an enable failure. Do not also call fly_to_location, set_layer_visibility, analyst_query, track_entity, set_context_mode, or control_cockpit for the same request. SELECT/FIND never implies Contacts or Cockpit unless the user explicitly asks for either mode.',
|
||||
'For ANALYTICAL questions about layer data — how many / which / fastest / highest / biggest / nearest flights, ships, fires, earthquakes, satellites, datacenters, or dams ("how many flights over Texas", "biggest fire near LA", "which ships are headed to Oakland", "anything above 40,000 feet") — call analyst_query, not get_entity_context. Narrate the count plus two or three notable examples by name, and reflect the result\'s coverage note honestly: the answer covers data loaded by enabled layers, not the whole world. If the needed layer is disabled, say so and offer to enable it. For follow-ups about the same set ("which of THOSE is closest?"), call analyst_query with followUp=true and only the new filter/sort.',
|
||||
'FEED STATE CONTRACT — analyst_query and get_current_view_state carry feedProvenance from the same layerFeedState the Data Layers chips use (nominal/loading/degraded/stale/fallback/unavailable, or off). NEVER narrate a count as live or current when feedProvenance.overall is not nominal. Say the feedState in the same breath as the count ("12 flights in view, STALE"). If unavailable, do not invent a count; say the feed is unavailable. Fallback is valid data — name the fallback source, never call it an enable failure. Confirm only what the tool result returned.',
|
||||
'FEED STATE CONTRACT — analyst_query and get_current_view_state carry feedProvenance from the same layerFeedState the Data Layers chips use (nominal/loading/degraded/stale/fallback/unavailable, or off). NEVER narrate a count as live or current when feedProvenance.overall is not nominal. Say the feedState in the same breath as the count ("12 flights in view, STALE"). If unavailable, do not invent a count; say the feed is unavailable. Fallback is valid data — name the fallback source, never call it an enable failure. Confirm only what the tool result returned.',
|
||||
'COUNTING CONTRACT — what "near" means. (1) While Contacts is ACTIVE, "near / nearby / how many aircraft" means the Contacts window: answer from contactsWindow in the tool result — those are the exact numbers on the user\'s panel. set_context_mode, analyst_query, and get_current_view_state carry it after Contacts settles. For "Open Contacts and tell me how many aircraft are within 250 km", call set_context_mode{mode:"contacts"} first and answer from contactsWindow.aircraft; do not answer from a pre-Contacts analyst query. analyst_query\'s own count measures currently-loaded records and is usually lower; never give it as the window count. CENTER PRECEDENCE for a nearby/how-many ask, in order: an explicit place in the question ("over Texas", "near Austin") always wins and ignores Contacts state; else the CONTACTS SUBJECT when Contacts is active and has one — a selected datacenter, dam, fire, or cable does NOT silently become the center; else an entity the user explicitly names ("around this datacenter"); else the current view, said aloud ("nothing is selected, so this is the current view"). With Contacts active but NO subject yet, use the view and say so; never read an empty panel. (2) With Contacts OFF, "nearby" means in view; "near <place>" means a radius around that place. (3) EVERY count names its scope in words — "42 in your window", "8 in view", "about 30 within 250 km of Austin" — never a bare number; analyst_query returns scopeLabel for exactly this. Two different numbers with named scopes are not a contradiction; say both if asked. (4) State counts VERBATIM — never estimate, round, or hedge ("a few", "less than a dozen"): if a tool returns 46, say 46. (5) When it matters, add once: counts cover loaded data, and the flights layer loads where you look.',
|
||||
'While Cockpit is active, navigate with control_cockpit (next/previous, optionally targetLayer or aircraftClass). track_entity and fly_to_location are REFUSED by design while Cockpit owns the camera — that refusal is correct, not an error to retry. To go somewhere else, exit Cockpit first. control_cockpit enter establishes Contacts itself, so do not call set_context_mode before or after it.',
|
||||
'When the target layer is unknown, OMIT layerId in track_entity so it searches all enabled layers. Passing the wrong layerId ("flights" for a military contact) returns "Nothing matched" even though the contact is loaded.',
|
||||
|
||||
@@ -572,7 +572,8 @@ export const ACTION_DESCRIPTIONS = {
|
||||
},
|
||||
},
|
||||
next_satellite_pass: {
|
||||
description: 'Predict the next pass in 24 hours for one satellite in the loaded catalog, identified by exact NORAD ID or name. Ambiguous names return candidates: ask for a choice rather than selecting one. Defaults to geometric passes; visibleOnly requires estimated illumination and a dark observer sky, which does not guarantee naked-eye visibility. Uses camera location unless coordinates are supplied.',
|
||||
description:
|
||||
'Predict the next pass in 24 hours for one satellite in the loaded catalog, identified by exact NORAD ID or name. Ambiguous names return candidates: ask for a choice rather than selecting one. Defaults to geometric passes; visibleOnly requires estimated illumination and a dark observer sky, which does not guarantee naked-eye visibility. Uses camera location unless coordinates are supplied.',
|
||||
$position: 1,
|
||||
},
|
||||
};
|
||||
+30
-11
@@ -32,7 +32,10 @@ export const LAYER_FEED_STATE_LABELS = Object.freeze({
|
||||
});
|
||||
|
||||
/** Snapshot feed-state including a disabled layer. */
|
||||
export const SNAPSHOT_FEED_STATES = Object.freeze([...LAYER_FEED_STATES, 'off']);
|
||||
export const SNAPSHOT_FEED_STATES = Object.freeze([
|
||||
...LAYER_FEED_STATES,
|
||||
'off',
|
||||
]);
|
||||
|
||||
/**
|
||||
* Lower rank is more severe. Voice/HUD `overall` is the worst queried state
|
||||
@@ -132,7 +135,9 @@ export function layerSnapshot(layer = {}, { now = Date.now() } = {}) {
|
||||
const enabled = Boolean(layer.enabled);
|
||||
const countRaw = Number(stats.count);
|
||||
const count = Number.isFinite(countRaw) ? countRaw : 0;
|
||||
const lastUpdate = Number.isFinite(Number(stats.lastUpdate)) ? Number(stats.lastUpdate) : null;
|
||||
const lastUpdate = Number.isFinite(Number(stats.lastUpdate))
|
||||
? Number(stats.lastUpdate)
|
||||
: null;
|
||||
const feedState = enabled ? layerFeedState({ ...stats, source }) : 'off';
|
||||
return {
|
||||
id: layer.id || layer.layerKey || null,
|
||||
@@ -179,14 +184,22 @@ export function feedProvenanceNote(layers = [], overall = null) {
|
||||
return 'Queried enabled layers are nominal. Still name the count scope; do not claim whole-world coverage.';
|
||||
}
|
||||
if (overall === 'off') {
|
||||
const names = layers.map((s) => s.name || s.id).filter(Boolean).join(', ') || 'the requested layer';
|
||||
const names =
|
||||
layers
|
||||
.map((s) => s.name || s.id)
|
||||
.filter(Boolean)
|
||||
.join(', ') || 'the requested layer';
|
||||
return `${names} is off. Do not invent a count; say the layer is off and offer to enable it.`;
|
||||
}
|
||||
const flagged = layers.filter((s) => (
|
||||
s.feedState && s.feedState !== 'nominal' && s.feedState !== 'off'
|
||||
));
|
||||
const flagged = layers.filter(
|
||||
(s) => s.feedState && s.feedState !== 'nominal' && s.feedState !== 'off',
|
||||
);
|
||||
const bits = flagged.map((s) => {
|
||||
const extras = [s.source, s.ageLabel || snapshotAgeLabel(s.lastUpdate), s.error]
|
||||
const extras = [
|
||||
s.source,
|
||||
s.ageLabel || snapshotAgeLabel(s.lastUpdate),
|
||||
s.error,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(', ');
|
||||
return `${s.name || s.id} is ${String(s.feedState).toUpperCase()}${extras ? ` (${extras})` : ''}`;
|
||||
@@ -201,11 +214,17 @@ export function feedProvenanceNote(layers = [], overall = null) {
|
||||
* @param {{ now?: number }} [options]
|
||||
* @returns {{ overall: string|null, layers: Array<object>, note: string }}
|
||||
*/
|
||||
export function feedProvenanceEnvelope(snapshots = [], { now = Date.now() } = {}) {
|
||||
const layers = (Array.isArray(snapshots) ? snapshots : []).map((entry) => asSnapshot(entry, now));
|
||||
export function feedProvenanceEnvelope(
|
||||
snapshots = [],
|
||||
{ now = Date.now() } = {},
|
||||
) {
|
||||
const layers = (Array.isArray(snapshots) ? snapshots : []).map((entry) =>
|
||||
asSnapshot(entry, now),
|
||||
);
|
||||
const active = layers.filter((s) => s.feedState && s.feedState !== 'off');
|
||||
const overall = worstFeedState(active.map((s) => s.feedState))
|
||||
|| (layers.length ? 'off' : null);
|
||||
const overall =
|
||||
worstFeedState(active.map((s) => s.feedState)) ||
|
||||
(layers.length ? 'off' : null);
|
||||
return {
|
||||
overall,
|
||||
layers,
|
||||
|
||||
+151
-73
@@ -20,45 +20,87 @@ import {
|
||||
const NOW = 1_700_000_000_000;
|
||||
|
||||
test('layerSnapshot stays Cesium-free so the HUD proxy can import it', () => {
|
||||
const src = readFileSync(new URL('./layerSnapshot.js', import.meta.url), 'utf8');
|
||||
const src = readFileSync(
|
||||
new URL('./layerSnapshot.js', import.meta.url),
|
||||
'utf8',
|
||||
);
|
||||
assert.equal(src.includes("from './manager.js'"), false);
|
||||
assert.doesNotMatch(src, /from ['"]cesium['"]/i);
|
||||
});
|
||||
|
||||
test('layerFeedState matches the chip classifier for every honesty class', () => {
|
||||
assert.equal(layerFeedState({ error: 'feed down', count: 0, lastUpdate: null }), 'unavailable');
|
||||
assert.equal(layerFeedState({
|
||||
status: 'unavailable',
|
||||
error: 'feed down',
|
||||
count: 50,
|
||||
lastUpdate: 1,
|
||||
}), 'unavailable');
|
||||
assert.equal(layerFeedState({ mode: 'sim', count: 100, lastUpdate: 1 }), 'fallback');
|
||||
assert.equal(layerFeedState({ source: 'adsb.lol', count: 10, lastUpdate: 1 }), 'fallback');
|
||||
assert.equal(layerFeedState({
|
||||
source: 'adsb.lol',
|
||||
fallback: false,
|
||||
count: 10,
|
||||
lastUpdate: 1,
|
||||
}), 'nominal');
|
||||
assert.equal(layerFeedState({ stale: true, count: 0, lastUpdate: 1 }), 'stale');
|
||||
assert.equal(layerFeedState({ error: 'partial group failure', count: 50, lastUpdate: 1 }), 'degraded');
|
||||
assert.equal(
|
||||
layerFeedState({ error: 'feed down', count: 0, lastUpdate: null }),
|
||||
'unavailable',
|
||||
);
|
||||
assert.equal(
|
||||
layerFeedState({
|
||||
status: 'unavailable',
|
||||
error: 'feed down',
|
||||
count: 50,
|
||||
lastUpdate: 1,
|
||||
}),
|
||||
'unavailable',
|
||||
);
|
||||
assert.equal(
|
||||
layerFeedState({ mode: 'sim', count: 100, lastUpdate: 1 }),
|
||||
'fallback',
|
||||
);
|
||||
assert.equal(
|
||||
layerFeedState({ source: 'adsb.lol', count: 10, lastUpdate: 1 }),
|
||||
'fallback',
|
||||
);
|
||||
assert.equal(
|
||||
layerFeedState({
|
||||
source: 'adsb.lol',
|
||||
fallback: false,
|
||||
count: 10,
|
||||
lastUpdate: 1,
|
||||
}),
|
||||
'nominal',
|
||||
);
|
||||
assert.equal(
|
||||
layerFeedState({ stale: true, count: 0, lastUpdate: 1 }),
|
||||
'stale',
|
||||
);
|
||||
assert.equal(
|
||||
layerFeedState({
|
||||
error: 'partial group failure',
|
||||
count: 50,
|
||||
lastUpdate: 1,
|
||||
}),
|
||||
'degraded',
|
||||
);
|
||||
assert.equal(layerFeedState({ loading: true }), 'loading');
|
||||
assert.equal(layerFeedState({ count: 5, lastUpdate: 1 }), 'nominal');
|
||||
assert.equal(layerFeedState({ status: 'zoom-in', error: 'zoom in to search', count: 12 }), 'nominal');
|
||||
assert.equal(layerFeedState({ status: 'zoom-in', stale: true, count: 12 }), 'stale');
|
||||
assert.equal(
|
||||
layerFeedState({
|
||||
status: 'zoom-in',
|
||||
error: 'zoom in to search',
|
||||
count: 12,
|
||||
}),
|
||||
'nominal',
|
||||
);
|
||||
assert.equal(
|
||||
layerFeedState({ status: 'zoom-in', stale: true, count: 12 }),
|
||||
'stale',
|
||||
);
|
||||
});
|
||||
|
||||
test('chip labels and severity cover every feed-state the envelope can emit', () => {
|
||||
assert.deepEqual([...LAYER_FEED_STATES], [
|
||||
'nominal', 'loading', 'degraded', 'stale', 'fallback', 'unavailable',
|
||||
]);
|
||||
assert.deepEqual(
|
||||
[...LAYER_FEED_STATES],
|
||||
['nominal', 'loading', 'degraded', 'stale', 'fallback', 'unavailable'],
|
||||
);
|
||||
assert.deepEqual([...SNAPSHOT_FEED_STATES], [...LAYER_FEED_STATES, 'off']);
|
||||
for (const state of LAYER_FEED_STATES) {
|
||||
assert.equal(typeof LAYER_FEED_STATE_LABELS[state], 'string');
|
||||
assert.equal(typeof FEED_STATE_SEVERITY[state], 'number');
|
||||
}
|
||||
assert.equal(FEED_STATE_SEVERITY.unavailable < FEED_STATE_SEVERITY.stale, true);
|
||||
assert.equal(
|
||||
FEED_STATE_SEVERITY.unavailable < FEED_STATE_SEVERITY.stale,
|
||||
true,
|
||||
);
|
||||
assert.equal(FEED_STATE_SEVERITY.stale < FEED_STATE_SEVERITY.fallback, true);
|
||||
assert.equal(worstFeedState(['nominal', 'stale', 'fallback']), 'stale');
|
||||
assert.equal(worstFeedState(['degraded', 'unavailable']), 'unavailable');
|
||||
@@ -66,13 +108,21 @@ test('chip labels and severity cover every feed-state the envelope can emit', ()
|
||||
});
|
||||
|
||||
test('a disabled layer is off even when leftover stats would read stale', () => {
|
||||
const snap = layerSnapshot({
|
||||
id: 'flights',
|
||||
name: 'Live Flights',
|
||||
enabled: false,
|
||||
source: 'OpenSky Network',
|
||||
stats: { stale: true, count: 12, lastUpdate: NOW - 240_000, source: 'OpenSky Network' },
|
||||
}, { now: NOW });
|
||||
const snap = layerSnapshot(
|
||||
{
|
||||
id: 'flights',
|
||||
name: 'Live Flights',
|
||||
enabled: false,
|
||||
source: 'OpenSky Network',
|
||||
stats: {
|
||||
stale: true,
|
||||
count: 12,
|
||||
lastUpdate: NOW - 240_000,
|
||||
source: 'OpenSky Network',
|
||||
},
|
||||
},
|
||||
{ now: NOW },
|
||||
);
|
||||
assert.equal(snap.feedState, 'off');
|
||||
assert.equal(snap.enabled, false);
|
||||
assert.equal(snap.count, 12);
|
||||
@@ -80,17 +130,20 @@ test('a disabled layer is off even when leftover stats would read stale', () =>
|
||||
});
|
||||
|
||||
test('an enabled stale layer carries age, source, and a mechanical note', () => {
|
||||
const snap = layerSnapshot({
|
||||
id: 'flights',
|
||||
name: 'Live Flights',
|
||||
enabled: true,
|
||||
stats: {
|
||||
stale: true,
|
||||
count: 12,
|
||||
lastUpdate: NOW - 240_000,
|
||||
source: 'OpenSky Network',
|
||||
const snap = layerSnapshot(
|
||||
{
|
||||
id: 'flights',
|
||||
name: 'Live Flights',
|
||||
enabled: true,
|
||||
stats: {
|
||||
stale: true,
|
||||
count: 12,
|
||||
lastUpdate: NOW - 240_000,
|
||||
source: 'OpenSky Network',
|
||||
},
|
||||
},
|
||||
}, { now: NOW });
|
||||
{ now: NOW },
|
||||
);
|
||||
assert.equal(snap.feedState, 'stale');
|
||||
assert.equal(snap.source, 'OpenSky Network');
|
||||
assert.equal(snap.ageSec, 240);
|
||||
@@ -104,20 +157,27 @@ test('an enabled stale layer carries age, source, and a mechanical note', () =>
|
||||
});
|
||||
|
||||
test('degraded and unavailable envelopes outrank leftover nominal neighbours', () => {
|
||||
const layers = layerSnapshots([
|
||||
{
|
||||
id: 'earthquakes',
|
||||
name: 'Earthquakes',
|
||||
enabled: true,
|
||||
stats: { count: 3, lastUpdate: NOW - 10_000 },
|
||||
},
|
||||
{
|
||||
id: 'local-firms',
|
||||
name: 'Active Fires',
|
||||
enabled: true,
|
||||
stats: { error: 'FIRMS key missing', count: 8, lastUpdate: NOW - 30_000 },
|
||||
},
|
||||
], { now: NOW });
|
||||
const layers = layerSnapshots(
|
||||
[
|
||||
{
|
||||
id: 'earthquakes',
|
||||
name: 'Earthquakes',
|
||||
enabled: true,
|
||||
stats: { count: 3, lastUpdate: NOW - 10_000 },
|
||||
},
|
||||
{
|
||||
id: 'local-firms',
|
||||
name: 'Active Fires',
|
||||
enabled: true,
|
||||
stats: {
|
||||
error: 'FIRMS key missing',
|
||||
count: 8,
|
||||
lastUpdate: NOW - 30_000,
|
||||
},
|
||||
},
|
||||
],
|
||||
{ now: NOW },
|
||||
);
|
||||
const envelope = feedProvenanceEnvelope(layers, { now: NOW });
|
||||
assert.equal(envelope.overall, 'degraded');
|
||||
assert.match(envelope.note, /Active Fires is DEGRADED/);
|
||||
@@ -125,12 +185,15 @@ test('degraded and unavailable envelopes outrank leftover nominal neighbours', (
|
||||
});
|
||||
|
||||
test('an unavailable feed with no prior data does not become a confident zero', () => {
|
||||
const snap = layerSnapshot({
|
||||
id: 'ais-live-vessels',
|
||||
name: 'Live Vessels',
|
||||
enabled: true,
|
||||
stats: { error: 'AISStream down', count: 0, lastUpdate: null },
|
||||
}, { now: NOW });
|
||||
const snap = layerSnapshot(
|
||||
{
|
||||
id: 'ais-live-vessels',
|
||||
name: 'Live Vessels',
|
||||
enabled: true,
|
||||
stats: { error: 'AISStream down', count: 0, lastUpdate: null },
|
||||
},
|
||||
{ now: NOW },
|
||||
);
|
||||
assert.equal(snap.feedState, 'unavailable');
|
||||
const envelope = feedProvenanceEnvelope([snap]);
|
||||
assert.equal(envelope.overall, 'unavailable');
|
||||
@@ -138,23 +201,38 @@ test('an unavailable feed with no prior data does not become a confident zero',
|
||||
});
|
||||
|
||||
test('fallback provenance names the fallback source instead of calling it live', () => {
|
||||
const snap = layerSnapshot({
|
||||
id: 'flights',
|
||||
name: 'Live Flights',
|
||||
enabled: true,
|
||||
stats: { source: 'adsb.lol fallback', count: 40, lastUpdate: NOW - 8_000 },
|
||||
}, { now: NOW });
|
||||
const snap = layerSnapshot(
|
||||
{
|
||||
id: 'flights',
|
||||
name: 'Live Flights',
|
||||
enabled: true,
|
||||
stats: {
|
||||
source: 'adsb.lol fallback',
|
||||
count: 40,
|
||||
lastUpdate: NOW - 8_000,
|
||||
},
|
||||
},
|
||||
{ now: NOW },
|
||||
);
|
||||
assert.equal(snap.feedState, 'fallback');
|
||||
assert.match(feedProvenanceNote([snap], 'fallback'), /adsb\.lol fallback/);
|
||||
});
|
||||
|
||||
test('view-state rows keep the existing identity fields and add feedState', () => {
|
||||
const row = viewStateLayerRow({
|
||||
id: 'flights',
|
||||
name: 'Live Flights',
|
||||
enabled: true,
|
||||
stats: { count: 9, lastUpdate: 42, error: null, source: 'OpenSky Network' },
|
||||
}, { now: NOW });
|
||||
const row = viewStateLayerRow(
|
||||
{
|
||||
id: 'flights',
|
||||
name: 'Live Flights',
|
||||
enabled: true,
|
||||
stats: {
|
||||
count: 9,
|
||||
lastUpdate: 42,
|
||||
error: null,
|
||||
source: 'OpenSky Network',
|
||||
},
|
||||
},
|
||||
{ now: NOW },
|
||||
);
|
||||
assert.deepEqual(row, {
|
||||
id: 'flights',
|
||||
name: 'Live Flights',
|
||||
|
||||
+50
-13
@@ -24,9 +24,18 @@ const AU_KM = 149597870.7;
|
||||
export function lookAnglesAt(satrec, dateMs, latDeg, lonDeg) {
|
||||
const date = new Date(dateMs);
|
||||
let pv;
|
||||
try { pv = propagate(satrec, date); } catch { return null; }
|
||||
try {
|
||||
pv = propagate(satrec, date);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
const pos = pv && pv.position;
|
||||
if (!pos || typeof pos === 'boolean' || ![pos.x, pos.y, pos.z].every(Number.isFinite)) return null;
|
||||
if (
|
||||
!pos ||
|
||||
typeof pos === 'boolean' ||
|
||||
![pos.x, pos.y, pos.z].every(Number.isFinite)
|
||||
)
|
||||
return null;
|
||||
const ecf = eciToEcf(pos, gstime(date));
|
||||
const look = ecfToLookAngles(
|
||||
{ latitude: latDeg * D2R, longitude: lonDeg * D2R, height: 0 },
|
||||
@@ -199,10 +208,31 @@ export function findNextSatellitePass({
|
||||
requireVisible = false,
|
||||
maxSunElevDeg = -6,
|
||||
}) {
|
||||
if (!satrec || ![latDeg, lonDeg, fromMs, minElevDeg, horizonHours, coarseStepSec, fineStepSec, maxSunElevDeg].every(Number.isFinite)
|
||||
|| Math.abs(latDeg) > 90 || Math.abs(lonDeg) > 180 || Math.abs(fromMs) > 8.63e15
|
||||
|| minElevDeg < 0 || minElevDeg > 90 || horizonHours <= 0 || horizonHours > 72
|
||||
|| coarseStepSec < 1 || coarseStepSec > 120 || fineStepSec < 0.1 || fineStepSec > 60) return null;
|
||||
if (
|
||||
!satrec ||
|
||||
![
|
||||
latDeg,
|
||||
lonDeg,
|
||||
fromMs,
|
||||
minElevDeg,
|
||||
horizonHours,
|
||||
coarseStepSec,
|
||||
fineStepSec,
|
||||
maxSunElevDeg,
|
||||
].every(Number.isFinite) ||
|
||||
Math.abs(latDeg) > 90 ||
|
||||
Math.abs(lonDeg) > 180 ||
|
||||
Math.abs(fromMs) > 8.63e15 ||
|
||||
minElevDeg < 0 ||
|
||||
minElevDeg > 90 ||
|
||||
horizonHours <= 0 ||
|
||||
horizonHours > 72 ||
|
||||
coarseStepSec < 1 ||
|
||||
coarseStepSec > 120 ||
|
||||
fineStepSec < 0.1 ||
|
||||
fineStepSec > 60
|
||||
)
|
||||
return null;
|
||||
const horizonMs = fromMs + horizonHours * 3600_000;
|
||||
const coarseMs = coarseStepSec * 1000;
|
||||
const fineMs = Math.max(1000, (fineStepSec || 5) * 1000);
|
||||
@@ -338,11 +368,16 @@ export function findNextSatellitePass({
|
||||
const denom = 2 * (peakLeftElev - 2 * maxElevDeg + peakRightElev);
|
||||
const shift = ((peakLeftElev - peakRightElev) / denom) * peakStepMs;
|
||||
if (Math.abs(shift) < peakStepMs) {
|
||||
refinedPeakMs = Math.max(riseMs, Math.min(setMs, Math.round(maxElevMs + shift)));
|
||||
refinedPeakElevDeg =
|
||||
Math.min(90, maxElevDeg -
|
||||
Math.pow(peakLeftElev - peakRightElev, 2) /
|
||||
(8 * (peakLeftElev - 2 * maxElevDeg + peakRightElev)));
|
||||
refinedPeakMs = Math.max(
|
||||
riseMs,
|
||||
Math.min(setMs, Math.round(maxElevMs + shift)),
|
||||
);
|
||||
refinedPeakElevDeg = Math.min(
|
||||
90,
|
||||
maxElevDeg -
|
||||
Math.pow(peakLeftElev - peakRightElev, 2) /
|
||||
(8 * (peakLeftElev - 2 * maxElevDeg + peakRightElev)),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -365,7 +400,10 @@ export function findNextSatellitePass({
|
||||
if (!hasVisibleSegment) {
|
||||
hasVisibleSegment = hasVisibleInterval(riseMs, setMs, (time) => {
|
||||
const look = lookAnglesAt(satrec, time, latDeg, lonDeg);
|
||||
return [Boolean(look && isSatelliteSunlit(look.satECI, time)), isObserverDark(latDeg, lonDeg, time, maxSunElevDeg)];
|
||||
return [
|
||||
Boolean(look && isSatelliteSunlit(look.satECI, time)),
|
||||
isObserverDark(latDeg, lonDeg, time, maxSunElevDeg),
|
||||
];
|
||||
});
|
||||
}
|
||||
|
||||
@@ -391,7 +429,6 @@ export function findNextSatellitePass({
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
/** Resolve shadow/twilight crossings to 20ms so short overlap is not lost between samples. */
|
||||
export function hasVisibleInterval(start, end, conditionsAt) {
|
||||
const visible = (states) => states[0] && states[1];
|
||||
|
||||
@@ -239,25 +239,44 @@ test('findNextSatellitePass samples the peak at fineStepSec', () => {
|
||||
assert.ok(Math.abs(fine.maxElevDeg - coarse.maxElevDeg) < 0.01);
|
||||
});
|
||||
|
||||
|
||||
test('invalid numerical search options return promptly without unbounded scans', () => {
|
||||
const satrec = twoline2satrec(L1, L2);
|
||||
const options = { satrec, ...AUSTIN, fromMs: FROM_MS };
|
||||
for (const change of [{ coarseStepSec: 0 }, { coarseStepSec: -1 }, { horizonHours: Infinity }, { horizonHours: 73 }, { fineStepSec: NaN }, { latDeg: 91 }, { fromMs: NaN }]) {
|
||||
for (const change of [
|
||||
{ coarseStepSec: 0 },
|
||||
{ coarseStepSec: -1 },
|
||||
{ horizonHours: Infinity },
|
||||
{ horizonHours: 73 },
|
||||
{ fineStepSec: NaN },
|
||||
{ latDeg: 91 },
|
||||
{ fromMs: NaN },
|
||||
]) {
|
||||
assert.equal(findNextSatellitePass({ ...options, ...change }), null);
|
||||
}
|
||||
});
|
||||
|
||||
test('visibility catches a short overlap between shadow exit and dawn', () => {
|
||||
assert.equal(hasVisibleInterval(0, 5000, (t) => [t >= 2400, t <= 2500]), true);
|
||||
assert.equal(hasVisibleInterval(0, 5000, (t) => [t >= 2500, t <= 2400]), false);
|
||||
assert.equal(
|
||||
hasVisibleInterval(0, 5000, (t) => [t >= 2400, t <= 2500]),
|
||||
true,
|
||||
);
|
||||
assert.equal(
|
||||
hasVisibleInterval(0, 5000, (t) => [t >= 2500, t <= 2400]),
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
test('a pass already underway clips rise to the requested start', () => {
|
||||
const satrec = twoline2satrec(L1, L2);
|
||||
const first = findNextSatellitePass({ satrec, ...AUSTIN, fromMs: FROM_MS });
|
||||
const clipped = findNextSatellitePass({ satrec, ...AUSTIN, fromMs: first.maxElevMs });
|
||||
const clipped = findNextSatellitePass({
|
||||
satrec,
|
||||
...AUSTIN,
|
||||
fromMs: first.maxElevMs,
|
||||
});
|
||||
assert.equal(clipped.riseMs, first.maxElevMs);
|
||||
assert.ok(clipped.setMs >= clipped.riseMs);
|
||||
assert.ok(clipped.maxElevMs >= clipped.riseMs && clipped.maxElevMs <= clipped.setMs);
|
||||
assert.ok(
|
||||
clipped.maxElevMs >= clipped.riseMs && clipped.maxElevMs <= clipped.setMs,
|
||||
);
|
||||
});
|
||||
@@ -657,15 +657,15 @@ test('the DISPLAY rail starts collapsed on a first run, and a stored choice wins
|
||||
// ── Voice: instruction-only, tool schema unchanged ─────────────────────
|
||||
|
||||
test('the voice TOOL SCHEMA matches the pinned release — the mission mapping is instructions only', () => {
|
||||
// ALPR deliberately adds its ID to the two layer menus and visibility aliases.
|
||||
// Analyst layers and the separate satellite-pass tool deliberately extend the schema.
|
||||
// Canonical serialization pins every tool name, description, property and
|
||||
// ordering while allowing source formatting. Derived from the unchanged
|
||||
// release schema before formatting (the previous source-byte pin passed).
|
||||
const block = JSON.stringify(GEV_REALTIME_TOOLS);
|
||||
assert.equal(block.length, 26208, 'serialized tool schema length drifted');
|
||||
assert.equal(block.length, 27271, 'serialized tool schema length drifted');
|
||||
assert.equal(
|
||||
crypto.createHash('sha256').update(block).digest('hex'),
|
||||
'135d4ec66239777da34a8476cdf8348574421d7afd2e981cc3490909a5bc8686',
|
||||
'da5f1bb334d1e882d63098cb35b29afde273947c0807ef6af2ef3d25a0df4918',
|
||||
'the first-run missions must ride EXISTING tools: no schema edit, no cache bust',
|
||||
);
|
||||
const instructions = fs.readFileSync(new URL('../server/providers/openai/instructions.js', import.meta.url), 'utf8');
|
||||
|
||||
+2
-2
@@ -20,7 +20,7 @@ import { CITY_POIS } from './locations.js';
|
||||
import { composeLocalityTag } from './hudLocality.js';
|
||||
import { ellipsoidalToMslDisplayM, ensureGeoidReady, geoidHeight } from './data/geoid.js';
|
||||
import { getBasemapLabelContext } from './voice/gevActions.js';
|
||||
import { hudSummaryLayerContext, hudTelemetryProvenanceTag, isHudSummaryUnconfigured } from './hudSummaryResponse.js';
|
||||
import { hudSummaryMatchesProvenance, hudSummaryLayerContext, hudTelemetryProvenanceTag, isHudSummaryUnconfigured } from './hudSummaryResponse.js';
|
||||
|
||||
/** Color palettes keyed by shader mode; applied as CSS custom properties. */
|
||||
const HUD_COLORS = {
|
||||
@@ -677,7 +677,7 @@ export class IntelHUD {
|
||||
if (!response.ok || !data?.summary) {
|
||||
throw new Error(data?.error || `HTTP ${response.status}`);
|
||||
}
|
||||
this._setSummaryText(data.summary, animate);
|
||||
this._setSummaryText(hudSummaryMatchesProvenance(data.summary, context.feedProvenance) ? data.summary : fallbackText, animate);
|
||||
} catch (error) {
|
||||
if (error?.name !== 'AbortError') {
|
||||
console.warn('[HUD] AI summary unavailable:', error);
|
||||
|
||||
@@ -95,3 +95,10 @@ export function hudTelemetryProvenanceTag(layers = [], options) {
|
||||
.slice(0, 2);
|
||||
return `${envelope.overall.toUpperCase()}${names.length ? ` ${names.join('/')}` : ''}`;
|
||||
}
|
||||
|
||||
/** Require the supplied non-nominal state before showing an AI summary. */
|
||||
export function hudSummaryMatchesProvenance(summary, provenance) {
|
||||
const state = provenance?.overall;
|
||||
if (!state || state === 'nominal' || state === 'off') return true;
|
||||
return String(summary || '').toUpperCase().split(/\W+/).includes(String(state).toUpperCase());
|
||||
}
|
||||
@@ -170,3 +170,10 @@ test('the HUD proxy uses the shared provenance instructions', () => {
|
||||
assert.match(local, /HUD_SUMMARY_INSTRUCTIONS/);
|
||||
assert.doesNotMatch(local, /enabled-layer text labels/);
|
||||
});
|
||||
|
||||
test('AI summaries missing a non-nominal provenance token fall back deterministically', async () => {
|
||||
const { hudSummaryMatchesProvenance } = await import('./hudSummaryResponse.js');
|
||||
assert.equal(hudSummaryMatchesProvenance('Austin flights operating normally today', { overall: 'stale' }), false);
|
||||
assert.equal(hudSummaryMatchesProvenance('Austin stale flights over downtown', { overall: 'stale' }), true);
|
||||
assert.equal(hudSummaryMatchesProvenance('Austin flights operating normally today', { overall: 'nominal' }), true);
|
||||
});
|
||||
@@ -204,14 +204,27 @@ export function createOrbits({ state: layerState, services, parts, source }) {
|
||||
const exactId = /^\d+$/.test(text) ? Number(text) : null;
|
||||
if (exactId !== null) {
|
||||
const sat = layerState._catalog.get(exactId);
|
||||
return sat?.satrec ? { status: 'ok', noradId: exactId, name: sat.name } : { status: 'not-found' };
|
||||
return sat?.satrec
|
||||
? { status: 'ok', noradId: exactId, name: sat.name }
|
||||
: { status: 'not-found' };
|
||||
}
|
||||
const q = text.toLowerCase();
|
||||
const rows = [...layerState._catalog].filter(([, sat]) => sat.satrec);
|
||||
const exact = rows.filter(([, sat]) => String(sat.name).toLowerCase() === q);
|
||||
const matches = exact.length ? exact : rows.filter(([, sat]) => String(sat.name).toLowerCase().includes(q));
|
||||
const exact = rows.filter(
|
||||
([, sat]) => String(sat.name).toLowerCase() === q,
|
||||
);
|
||||
const matches = exact.length
|
||||
? exact
|
||||
: rows.filter(([, sat]) => String(sat.name).toLowerCase().includes(q));
|
||||
if (!matches.length) return { status: 'not-found' };
|
||||
if (matches.length > 1) return { status: 'ambiguous', candidates: matches.slice(0, 8).map(([noradId, sat]) => ({ noradId, name: sat.name })), totalMatches: matches.length };
|
||||
if (matches.length > 1)
|
||||
return {
|
||||
status: 'ambiguous',
|
||||
candidates: matches
|
||||
.slice(0, 8)
|
||||
.map(([noradId, sat]) => ({ noradId, name: sat.name })),
|
||||
totalMatches: matches.length,
|
||||
};
|
||||
return { status: 'ok', noradId: matches[0][0], name: matches[0][1].name };
|
||||
}
|
||||
|
||||
|
||||
@@ -14,9 +14,15 @@ import { ISS_NORAD } from './policy.js';
|
||||
*/
|
||||
export function mapAnalystRecord(raw) {
|
||||
const num = (v) => (Number.isFinite(v) ? v : null);
|
||||
const text = (v) => { const t = String(v ?? '').trim(); return t || null; };
|
||||
const noradNum = raw?.noradId == null || raw.noradId === '' ? NaN : Number(raw.noradId);
|
||||
const noradId = Number.isFinite(noradNum) ? String(Math.trunc(noradNum)) : text(raw?.noradId);
|
||||
const text = (v) => {
|
||||
const t = String(v ?? '').trim();
|
||||
return t || null;
|
||||
};
|
||||
const noradNum =
|
||||
raw?.noradId == null || raw.noradId === '' ? NaN : Number(raw.noradId);
|
||||
const noradId = Number.isFinite(noradNum)
|
||||
? String(Math.trunc(noradNum))
|
||||
: text(raw?.noradId);
|
||||
const name = text(raw?.name);
|
||||
const group = text(raw?.group);
|
||||
const isIss = noradNum === ISS_NORAD;
|
||||
@@ -30,54 +36,64 @@ export function mapAnalystRecord(raw) {
|
||||
speedMps: num(raw?.speedMps),
|
||||
// Empty records stay class-less; the class table's fallback is VISUAL and
|
||||
// must not leak onto a row that has no satellite identity.
|
||||
satelliteClass: (noradId || group) ? satelliteClassLabel(group, { isIss }) : null,
|
||||
satelliteClass:
|
||||
noradId || group ? satelliteClassLabel(group, { isIss }) : null,
|
||||
group,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
/** Create on-demand analyst snapshots owned by one layer instance. */
|
||||
export function createRecords({ state, parts }) {
|
||||
function analystGeo(noradId, sat, now) {
|
||||
const pos = sat?.satrec ? parts.orbits.propagatePosition(sat.satrec, now) : null;
|
||||
if (pos && Number.isFinite(pos.latitude) && Number.isFinite(pos.longitude)) {
|
||||
function analystGeo(noradId, sat, now) {
|
||||
const pos = sat?.satrec
|
||||
? parts.orbits.propagatePosition(sat.satrec, now)
|
||||
: null;
|
||||
if (
|
||||
pos &&
|
||||
Number.isFinite(pos.latitude) &&
|
||||
Number.isFinite(pos.longitude)
|
||||
) {
|
||||
return {
|
||||
lat: pos.latitude,
|
||||
lon: pos.longitude,
|
||||
altitudeM: pos.altitude,
|
||||
speedMps: Number.isFinite(pos.speedMps) ? pos.speedMps : null,
|
||||
};
|
||||
}
|
||||
const point = state._points.get(noradId);
|
||||
if (!point?.position) return null;
|
||||
const carto = Cesium.Cartographic.fromCartesian(point.position);
|
||||
if (!carto) return null;
|
||||
return {
|
||||
lat: pos.latitude,
|
||||
lon: pos.longitude,
|
||||
altitudeM: pos.altitude,
|
||||
speedMps: Number.isFinite(pos.speedMps) ? pos.speedMps : null,
|
||||
lat: Cesium.Math.toDegrees(carto.latitude),
|
||||
lon: Cesium.Math.toDegrees(carto.longitude),
|
||||
altitudeM: carto.height,
|
||||
speedMps: null,
|
||||
};
|
||||
}
|
||||
const point = state._points.get(noradId);
|
||||
if (!point?.position) return null;
|
||||
const carto = Cesium.Cartographic.fromCartesian(point.position);
|
||||
if (!carto) return null;
|
||||
return {
|
||||
lat: Cesium.Math.toDegrees(carto.latitude),
|
||||
lon: Cesium.Math.toDegrees(carto.longitude),
|
||||
altitudeM: carto.height,
|
||||
speedMps: null,
|
||||
};
|
||||
}
|
||||
|
||||
function getAnalystRecords(maxCount = 2000) {
|
||||
if (!state._enabled || !state._catalog.size) return [];
|
||||
const limit = Number.isFinite(maxCount) ? Math.max(1, Math.floor(maxCount)) : 2000;
|
||||
const limit = Number.isFinite(maxCount)
|
||||
? Math.max(1, Math.floor(maxCount))
|
||||
: 2000;
|
||||
const now = new Date();
|
||||
const result = [];
|
||||
const emit = (noradId, sat) => {
|
||||
if (result.length >= limit) return false;
|
||||
const pos = analystGeo(noradId, sat, now);
|
||||
if (!pos) return true;
|
||||
result.push(mapAnalystRecord({
|
||||
noradId,
|
||||
name: sat?.name,
|
||||
group: sat?.group,
|
||||
lat: pos.lat,
|
||||
lon: pos.lon,
|
||||
altitudeM: pos.altitudeM,
|
||||
speedMps: pos.speedMps,
|
||||
}));
|
||||
result.push(
|
||||
mapAnalystRecord({
|
||||
noradId,
|
||||
name: sat?.name,
|
||||
group: sat?.group,
|
||||
lat: pos.lat,
|
||||
lon: pos.lon,
|
||||
altitudeM: pos.altitudeM,
|
||||
speedMps: pos.speedMps,
|
||||
}),
|
||||
);
|
||||
return result.length < limit;
|
||||
};
|
||||
for (const [noradId, sat] of state._catalog) {
|
||||
|
||||
@@ -177,17 +177,20 @@ test('no unchanged Realtime tool definition drifts silently', () => {
|
||||
'fly_to_location',
|
||||
'select_nearest_aircraft',
|
||||
'set_map_stack',
|
||||
'analyst_query',
|
||||
'next_iss_pass',
|
||||
'next_satellite_pass',
|
||||
]);
|
||||
const unchanged = realtimeTools()
|
||||
.filter((tool) => !TOUCHED.has(tool.name))
|
||||
.sort((a, b) => a.name.localeCompare(b.name));
|
||||
assert.equal(unchanged.length, 21);
|
||||
assert.equal(unchanged.length, 19);
|
||||
const digest = createHash('sha256')
|
||||
.update(JSON.stringify(unchanged))
|
||||
.digest('hex')
|
||||
.slice(0, 16);
|
||||
// ALPR intentionally extends the two layer enums; retain the complete pin.
|
||||
assert.equal(digest, '6963175a0c9a76de', 'an unchanged Realtime tool definition drifted');
|
||||
// Analyst additions and ISS wording correction are explicitly excluded above; all other tool definitions retain their pin.
|
||||
assert.equal(digest, '7411ea585e534795', 'an unchanged Realtime tool definition drifted');
|
||||
});
|
||||
|
||||
test('Radio volume and mission speed share the Sharpen slider visual language', () => {
|
||||
|
||||
@@ -866,7 +866,9 @@ const schemas = [
|
||||
{
|
||||
name: 'next_satellite_pass',
|
||||
parameters: {
|
||||
type: 'object', additionalProperties: false, required: ['target'],
|
||||
type: 'object',
|
||||
additionalProperties: false,
|
||||
required: ['target'],
|
||||
properties: {
|
||||
target: { type: 'string', minLength: 1, maxLength: 120 },
|
||||
latitude: { type: 'number', minimum: -90, maximum: 90 },
|
||||
|
||||
@@ -15,13 +15,13 @@ 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 and satellite release', () => {
|
||||
const digest = createHash('sha256')
|
||||
.update(JSON.stringify(stable(GEV_REALTIME_TOOLS)))
|
||||
.digest('hex');
|
||||
assert.equal(
|
||||
digest,
|
||||
'956381c3456d3644ed7c9cda72910dc68a34d9191e0b3e414ee200c348245214',
|
||||
'9e33ac0fa5a860a17bb6d79f2c43646b6c36d0c932590bf114814e891a1c96de',
|
||||
);
|
||||
});
|
||||
|
||||
@@ -76,3 +76,12 @@ 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) => tool.name !== 'next_satellite_pass');
|
||||
const layers = legacy.find((tool) => tool.name === 'analyst_query').parameters.properties.layers.items;
|
||||
layers.enum = layers.enum.filter((key) => !['satellites', 'local-datacenters', 'local-dams'].includes(key));
|
||||
// Independently derived by executing trusted c9f9896 actionSchemas in the restricted container.
|
||||
assert.equal(createHash('sha256').update(JSON.stringify(legacy)).digest('hex'), '820fff21658f6907e1010b2b79c5431a77f4e34afd2277d62d8de46c368b6f8c');
|
||||
});
|
||||
Reference in new issue
Block a user