fix: pin additive voice contracts and require HUD provenance wording

This commit is contained in:
Bilawal Sidhu committed 2026-09-15 02:28:41 -05:00
1 parent 6d6d43c36c
commit 1e8f919cf3
16 files changed
+368 -154

No files matched your search

+4 -1
View File
@@ -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';
+1 -1
View File
@@ -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.',
+2 -1
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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];
+25 -6
View File
@@ -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,
);
});
+3 -3
View File
@@ -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
View File
@@ -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);
+7
View File
@@ -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());
}
+7
View File
@@ -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);
});
+17 -4
View File
@@ -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 };
}
+49 -33
View File
@@ -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) {
+6 -3
View File
@@ -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', () => {
+3 -1
View File
@@ -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 },
+11 -2
View File
@@ -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');
});