feat(voice): attach feed-state provenance to analyst and HUD answers

Share one layer snapshot envelope so analyst_query, get_current_view_state,
and HUD summaries narrate the same chip feed-state instead of treating stale
or degraded feeds as live.

Co-authored-by: Matt Van Horn <[email protected]>
This commit is contained in:
Cursor AgentandMatt Van Horn committed 2026-09-12 16:00:43 +00:00
1 parent aacfa06a31
commit cfb0d6e247
15 files changed
+881 -118

No files matched your search

+5
View File
@@ -13,6 +13,11 @@ of current runtime behavior, see [`docs/CURRENT-STATE.md`](docs/CURRENT-STATE.md
## [Unreleased]
- Attach honest layer feed-state provenance (nominal/loading/degraded/stale/
fallback/unavailable) to `analyst_query`, `get_current_view_state`, and HUD
summary context so voice and HUD answers cannot narrate a stale or degraded
feed as live.
- Separate terrain, traffic, FIRMS and GBFS middleware into focused provider
modules, preserving local configuration, routes and cache/error behavior.
+18 -2
View File
@@ -1,5 +1,20 @@
# God's Eye View Current State
## Voice and HUD feed-state provenance
Voice and HUD answers share one layer snapshot envelope (`src/data/layerSnapshot.js`)
so they cannot narrate a stale, degraded, fallback, loading, or unavailable feed as
live. `layerFeedState` — the same classifier the Data Layers chips use — stamps
each queried or enabled layer with `nominal` / `loading` / `degraded` / `stale` /
`fallback` / `unavailable`, or `off` when the layer is disabled. `analyst_query`
and `get_current_view_state` carry `feedProvenance` (`overall`, per-layer rows,
and a mechanical `note`). The HUD summary context sends the same snapshots; the
five-word AI line must include a non-nominal feedState token, and the
deterministic telemetry fallback appends e.g. `STALE LIVE FLIGHTS`. Confirmations
still report only what the tool result returned.
## Terrain, traffic, fire and bike-share provider modules
## Terrain, traffic, fire and bike-share provider modules
Local composition now imports separate Node modules for Re:Earth heights,
@@ -2426,13 +2441,14 @@ silently demoting every later lookup for the session.
2. **Contacts OFF** → "nearby" means **in view**; "near \<place\>" means a radius around that place. A radius query with Contacts active and no explicit centre is centred on the **active contact**, not the camera.
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` so this is mechanical. Two different numbers with named scopes are not a contradiction.
4. **The loaded-data caveat is stated once when relevant**: counts cover loaded data, and the flights layer loads where you look (appended to `coverage.note` for radius/view scopes over viewport-loaded layers).
5. **Feed-state provenance rides with every count**: `feedProvenance` is the same `layerFeedState` the chips show. A non-nominal `overall` must be spoken with the count ("12 flights in view, STALE"); an unavailable feed is not a confident zero; fallback is valid data and must be named as fallback. Mechanism: `src/data/layerSnapshot.js`, attached by `analystEngine` / `get_current_view_state` / HUD summary context.
- **Degradation**: without `OPENAI_API_KEY`, `/api/realtime/token` returns 503 and the mic button surfaces the error; the rest of the app is unaffected.
### AI HUD Summary (June 2026)
- HUD `SUMMARY` readout requests a five-word intelligence-style summary from `/api/openai/hud-summary` (model `OPENAI_HUD_SUMMARY_MODEL`, default `gpt-5-nano`, minimal reasoning).
- Input is the live basemap label context (place/street/nearby-place labels + enabled layers) — the model is instructed not to infer from coordinates.
- Output is sanitized to exactly five words; falls back to the deterministic telemetry summary on error/timeout (5s abort); typewriter animation on update.
- Input is the live basemap label context (place/street/nearby-place labels + enabled layers with `feedState`) — the model is instructed not to infer from coordinates or invent a feed-state. A non-nominal enabled layer requires that feedState token in the five words.
- Output is sanitized to exactly five words; falls back to the deterministic telemetry summary on error/timeout (5s abort), which itself appends a provenance tag (`STALE LIVE FLIGHTS`) when an enabled feed is not nominal; typewriter animation on update.
### Map Stack Switcher (June 2026)
+6 -11
View File
@@ -61,7 +61,7 @@ import {
normalizeRegionalPlace,
normalizeRegionalWeather,
} from '../../src/data/regionalBrief.js';
import { keylessHudSummaryResponse } from '../../src/hudSummaryResponse.js';
import { HUD_SUMMARY_INSTRUCTIONS, keylessHudSummaryResponse } from '../../src/hudSummaryResponse.js';
import { parseEnv as parseDotenvText } from 'node:util';
import { readEnvironmentSource as readPinokioEnvironmentSource } from '../../scripts/pinokio-environment.mjs';
import {
@@ -2767,13 +2767,7 @@ export function openAiRealtimeProxy() {
},
body: JSON.stringify({
model: process.env.OPENAI_HUD_SUMMARY_MODEL || OPENAI_HUD_SUMMARY_MODEL_DEFAULT,
instructions: [
"Write one concise intelligence-HUD summary for God's Eye View.",
'Use only the supplied place, street, nearby-place, and enabled-layer text labels.',
'Prefer the clearest named place and include a relevant enabled layer only when useful.',
'Do not infer from coordinates or invent a place.',
'Output exactly five words with no title, punctuation, markdown, or introductory phrase.',
].join(' '),
instructions: HUD_SUMMARY_INSTRUCTIONS,
input: JSON.stringify(context),
reasoning: { effort: 'minimal' },
max_output_tokens: 100,
@@ -2905,8 +2899,9 @@ export function openAiRealtimeProxy() {
'For camera-motion requests — "orbit around this", "pan left", "tilt up", "stop moving" — call move_camera. For "fly the route" over a drawn route, call fly_route. Confirm with the RESULTING state ("Orbiting slowly", "Flying the route").',
'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, or earthquakes ("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.',
'For ANALYTICAL questions about layer data — how many / which / fastest / highest / biggest / nearest flights, ships, fires, or earthquakes ("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 and feedProvenance honestly: the answer covers data loaded by enabled layers, not the whole world, and a non-nominal feedState is not live. 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.',
'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.',
'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.',
'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.',
'If get_entity_context has no selected object or overlay entities, use its basemap context: Google Photorealistic 3D Tiles/Cesium source, center target coordinates, reverse-geocoded place, camera altitude, active style, and enabled layers. Do not say there is nothing unless the basemap target is also unavailable.',
@@ -3314,7 +3309,7 @@ const GEV_REALTIME_TOOLS = [
{
type: 'function',
name: 'get_current_view_state',
description: 'Read the current camera, style, Context, Cockpit, HUD, detection, map stack, post-processing, scene-playback, tracked-entity, and layer state before choosing another action.',
description: 'Read the current camera, style, Context, Cockpit, HUD, detection, map stack, post-processing, scene-playback, tracked-entity, and layer state before choosing another action. Each layer includes feedState (nominal/loading/degraded/stale/fallback/unavailable/off) plus feedProvenance; narrate layer counts only with that provenance.',
parameters: {
type: 'object',
additionalProperties: false,
@@ -3608,7 +3603,7 @@ const GEV_REALTIME_TOOLS = [
{
type: 'function',
name: 'analyst_query',
description: 'Answer questions ABOUT the data currently loaded on the map — counts, lists, superlatives, and attribute filters over live layers (flights, military, ships, fires, earthquakes). Examples: "how many flights over Texas", "biggest fire near LA", "which ships are headed to Oakland", "anything above 40,000 feet", "fastest thing in view". Queries ONLY client-side data from ENABLED layers — if the needed layer is off, say so and offer to enable it. For a follow-up about the previous answer\'s set ("which of those is closest?"), set followUp=true and send only the new filters/sort.',
description: 'Answer questions ABOUT the data currently loaded on the map — counts, lists, superlatives, and attribute filters over live layers (flights, military, ships, fires, earthquakes). Examples: "how many flights over Texas", "biggest fire near LA", "which ships are headed to Oakland", "anything above 40,000 feet", "fastest thing in view". Queries ONLY client-side data from ENABLED layers — if the needed layer is off, say so and offer to enable it. Results include feedProvenance (the same feedState the Data Layers chips show). Narrate counts only with that provenance; never describe a stale, degraded, loading, fallback, or unavailable feed as live. For a follow-up about the previous answer\'s set ("which of those is closest?"), set followUp=true and send only the new filters/sort.',
parameters: {
type: 'object',
additionalProperties: false,
+31 -4
View File
@@ -18,14 +18,21 @@
*
* Providers (injected — keeps the engine pure and node-testable):
* getRecords(layerKey) → Array<record> (layer accessor snapshot)
* getLayerSnapshot?(layerKey) → snapshot|null (honest feed-state envelope)
* resolveRegionRing(name) → Promise<{ring, name}|null> (NE pack / admin boundary)
* getViewContext() → {lat, lon, viewRadiusKm, bounds?} (camera-derived)
*
* @module data/analystEngine
*/
import { feedProvenanceEnvelope } from './layerSnapshot.js';
import { pointInRing } from './naturalEarthRegions.js';
function withFeedProvenance(coverage, snapshots) {
if (!Array.isArray(snapshots) || snapshots.length === 0) return coverage;
return { ...coverage, feedProvenance: feedProvenanceEnvelope(snapshots) };
}
/** Layers the engine understands, with the fields queries may reference. */
export const ANALYST_LAYERS = {
flights: { numeric: ['altitudeM', 'speedMps', 'verticalRateMps'], text: ['callsign', 'icao24', 'originCountry', 'operator', 'routeOrigin', 'routeDestination', 'aircraftClass'], flags: ['military', 'onGround'] },
@@ -115,12 +122,15 @@ export function createAnalystEngine(providers) {
// 1) Source records
let records;
let layersQueried;
let queriedSnapshots;
if (layers === null) {
records = lastResult.items.slice();
layersQueried = lastResult.coverage.layersQueried;
queriedSnapshots = lastResult.coverage.feedProvenance?.layers || [];
} else {
records = [];
layersQueried = [];
queriedSnapshots = [];
const unknown = layers.filter((k) => !ANALYST_LAYERS[k]);
if (unknown.length) {
return {
@@ -132,7 +142,21 @@ export function createAnalystEngine(providers) {
for (const key of layers) {
if (!ANALYST_LAYERS[key]) continue;
const rows = providers.getRecords(key) || [];
layersQueried.push({ layerKey: key, records: rows.length });
const snapshot = typeof providers.getLayerSnapshot === 'function'
? providers.getLayerSnapshot(key)
: null;
if (snapshot) queriedSnapshots.push(snapshot);
layersQueried.push({
layerKey: key,
records: rows.length,
...(snapshot ? {
feedState: snapshot.feedState,
source: snapshot.source,
lastUpdate: snapshot.lastUpdate,
enabled: snapshot.enabled,
error: snapshot.error,
} : {}),
});
for (const row of rows) records.push({ layerKey: key, ...row });
}
}
@@ -151,7 +175,10 @@ export function createAnalystEngine(providers) {
return {
ok: false,
error: `I couldn't resolve a boundary for "${scope.name}" — try a state, country, or a named natural region.`,
coverage: { layersQueried, scope: `region:${scope.name}:unresolved` },
coverage: withFeedProvenance({
layersQueried,
scope: `region:${scope.name}:unresolved`,
}, queriedSnapshots),
};
}
resolvedScope = region;
@@ -225,12 +252,12 @@ export function createAnalystEngine(providers) {
truncated: items.length > top.length,
summary: summarize(items, sortBy && sortBy !== 'distance' ? sortBy : null),
scopeLabel,
coverage: {
coverage: withFeedProvenance({
layersQueried,
scope: scopeNote,
followUp: Boolean(spec.followUp && lastResult),
note: 'client-side data only — answers cover what the enabled layers currently hold',
},
}, queriedSnapshots),
// Surfaced so the narration can name the centre it measured from rather
// than implying a view-centred answer.
...(resolvedScope?.centeredOn ? { centeredOn: resolvedScope.centeredOn } : {}),
+48 -4
View File
@@ -1,6 +1,7 @@
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { createAnalystEngine, applyScope, haversineKm } from './analystEngine.js';
import { layerSnapshot } from './layerSnapshot.js';
// Stub world: a square "Texland" region, flights + ships + fires around it.
const TEXLAND = { name: 'Texland', ring: [[-100, 28], [-94, 28], [-94, 33], [-100, 33]] };
@@ -20,18 +21,31 @@ const FIRES = [
{ id: 'FIRE-3', lat: 51.9, lon: -121.9, frp: 2400 },
];
function makeEngine() {
function snapshotFor(key, rows, extraStats = {}) {
return layerSnapshot({
id: key,
name: key,
enabled: true,
stats: { count: rows.length, lastUpdate: 1, ...extraStats },
});
}
function makeEngine(statOverrides = {}) {
const records = { flights: FLIGHTS, 'ais-live-vessels': SHIPS, 'local-firms': FIRES };
return createAnalystEngine({
getRecords: (key) => ({ flights: FLIGHTS, 'ais-live-vessels': SHIPS, 'local-firms': FIRES }[key] || []),
getRecords: (key) => records[key] || [],
getLayerSnapshot: (key) => snapshotFor(key, records[key] || [], statOverrides[key]),
resolveRegionRing: async (name) => (/texland/i.test(name) ? TEXLAND : null),
getViewContext: () => ({ lat: 30.27, lon: -97.74, viewRadiusKm: 150 }),
});
}
/** Same world, but Contacts is up with a subject far from the parked camera. */
function makeContactsEngine(subject) {
function makeContactsEngine(subject, statOverrides = {}) {
const records = { flights: FLIGHTS, 'ais-live-vessels': SHIPS, 'local-firms': FIRES };
return createAnalystEngine({
getRecords: (key) => ({ flights: FLIGHTS, 'ais-live-vessels': SHIPS, 'local-firms': FIRES }[key] || []),
getRecords: (key) => records[key] || [],
getLayerSnapshot: (key) => snapshotFor(key, records[key] || [], statOverrides[key]),
resolveRegionRing: async (name) => (/texland/i.test(name) ? TEXLAND : null),
// Parked far away, as a high-altitude camera often is.
getViewContext: () => ({ lat: 45.0, lon: -122.0, viewRadiusKm: 150 }),
@@ -201,3 +215,33 @@ test('helpers: haversine sanity + scope radius', () => {
const scoped = applyScope(FLIGHTS, { kind: 'radius' }, { center: { lat: 30.27, lon: -97.74 }, km: 50 });
assert.deepEqual(scoped.map((f) => f.id).sort(), ['GND1', 'SWA1']);
});
test('analyst: a stale feed stamps provenance so the count cannot be narrated as live', async () => {
const r = await makeEngine({ flights: { stale: true } }).query({
layers: ['flights'], scope: { kind: 'view' }, limit: 50,
});
assert.equal(r.ok, true);
assert.equal(r.coverage.layersQueried[0].feedState, 'stale');
assert.equal(r.coverage.feedProvenance.overall, 'stale');
assert.match(r.coverage.feedProvenance.note, /STALE/);
assert.match(r.coverage.feedProvenance.note, /do not describe this as live/i);
});
test('analyst: a degraded feed outranks a nominal neighbour in overall provenance', async () => {
const r = await makeEngine({
'local-firms': { error: 'FIRMS timeout', count: 2 },
}).query({
layers: ['flights', 'local-firms'], scope: { kind: 'anywhere' }, limit: 50,
});
assert.equal(r.coverage.feedProvenance.overall, 'degraded');
const fires = r.coverage.layersQueried.find((row) => row.layerKey === 'local-firms');
assert.equal(fires.feedState, 'degraded');
});
test('analyst: follow-up keeps the original feed-state snapshot', async () => {
const eng = makeEngine({ flights: { stale: true } });
await eng.query({ layers: ['flights'], scope: { kind: 'region', name: 'Texland' } });
const r = await eng.query({ followUp: true, filters: [{ field: 'onGround', op: 'eq', value: true }] });
assert.equal(r.coverage.followUp, true);
assert.equal(r.coverage.feedProvenance.overall, 'stale');
});
+278
View File
@@ -0,0 +1,278 @@
/**
* Shared layer snapshot envelope for voice and HUD answers.
*
* Data Layers chips, `analyst_query`, `get_current_view_state`, and the HUD
* summary must all name the same feed-state. This module is the one envelope:
* it classifies `getStats()` through `layerFeedState` and stamps the result
* so a stale or degraded feed cannot be narrated as live. Cesium-free so the
* HUD summary proxy can import it from Node.
*
* @module data/layerSnapshot
*/
/** Feed states the Data Layers chips and voice/HUD provenance share. */
export const LAYER_FEED_STATES = Object.freeze([
'nominal',
'loading',
'degraded',
'stale',
'fallback',
'unavailable',
]);
/** Chip labels for enabled layers. Disabled layers read OFF, not a feed-state. */
export const LAYER_FEED_STATE_LABELS = Object.freeze({
nominal: 'ON',
loading: 'LOADING',
degraded: 'DEGRADED',
stale: 'STALE',
fallback: 'FALLBACK',
unavailable: 'UNAVAILABLE',
});
/** Snapshot feed-state including a disabled layer. */
export const SNAPSHOT_FEED_STATES = Object.freeze([...LAYER_FEED_STATES, 'off']);
/**
* Lower rank is more severe. Voice/HUD `overall` is the worst queried state
* so a mixed roster cannot hide an unavailable or stale feed behind a nominal
* neighbour.
*/
export const FEED_STATE_SEVERITY = Object.freeze({
unavailable: 0,
loading: 1,
degraded: 2,
stale: 3,
fallback: 4,
nominal: 5,
off: 6,
});
/**
* Normalize heterogeneous layer stats into one honest control-chip state.
* @param {object|null} stats Layer getStats() result.
* @returns {'nominal'|'loading'|'degraded'|'stale'|'fallback'|'unavailable'} Feed state.
*/
export function layerFeedState(stats = {}) {
const state = stats || {};
const status = typeof state.status === 'string' ? state.status.toLowerCase() : '';
const source = `${state.source || ''} ${state.coverage || ''}`;
const hasExplicitFallback = typeof state.fallback === 'boolean';
const hasPriorData = Number(state.count) > 0 || Boolean(state.lastUpdate);
const presentedError = state.error || state.lastError || state.managerRefreshError;
if (['unavailable', 'offline', 'down', 'error'].includes(status)) return 'unavailable';
if (
(presentedError || state.unavailable === true || state.available === false)
&& !hasPriorData
&& !['zoom-in', 'empty', 'idle'].includes(status)
) {
return 'unavailable';
}
if (state.loading) return 'loading';
// Guidance states ask the user to act (zoom in, run a search) — normal
// operation, not feed faults. One honesty carve-out: layers keep their
// rendered records through the guidance state, so a genuinely stale cache
// still reads STALE; a guidance prompt alone never reads DEGRADED.
if (['zoom-in', 'empty', 'idle'].includes(status)) {
return state.stale ? 'stale' : 'nominal';
}
if (
state.fallback === true
|| status === 'fallback'
|| state.mode === 'sim'
|| /\bfallback\b/i.test(source)
|| (!hasExplicitFallback && /\badsb\.lol\b/i.test(source))
) {
return 'fallback';
}
if (state.stale || status === 'stale') return 'stale';
if (
state.degraded
|| presentedError
|| state.unavailable === true
|| state.available === false
) return 'degraded';
return 'nominal';
}
function snapshotError(stats) {
const err = stats?.error || stats?.lastError || stats?.managerRefreshError;
if (err == null || err === '') return null;
return err instanceof Error ? err.message : String(err);
}
/**
* Age of a lastUpdate timestamp in whole seconds.
* @param {number|null|undefined} lastUpdate Epoch ms.
* @param {number} [now=Date.now()] Clock for tests.
* @returns {number|null} Non-negative seconds, or null when unknown.
*/
export function snapshotAgeSec(lastUpdate, now = Date.now()) {
const stamp = Number(lastUpdate);
if (!Number.isFinite(stamp) || stamp <= 0) return null;
return Math.max(0, Math.floor((now - stamp) / 1000));
}
/**
* Compact age label matching the Data Layers meta line.
* @param {number|null|undefined} lastUpdate Epoch ms.
* @param {number} [now=Date.now()] Clock for tests.
* @returns {string|null} `just now` / `12s ago` / `4m ago` / `2h ago`.
*/
export function snapshotAgeLabel(lastUpdate, now = Date.now()) {
const sec = snapshotAgeSec(lastUpdate, now);
if (sec === null) return null;
if (sec < 5) return 'just now';
if (sec < 60) return `${sec}s ago`;
if (sec < 3600) return `${Math.floor(sec / 60)}m ago`;
return `${Math.floor(sec / 3600)}h ago`;
}
/**
* Pick the most severe feed-state from a list.
* @param {Iterable<string|null|undefined>} states Feed-state strings.
* @returns {'nominal'|'loading'|'degraded'|'stale'|'fallback'|'unavailable'|'off'|null}
*/
export function worstFeedState(states) {
let worst = null;
let worstRank = Number.POSITIVE_INFINITY;
for (const state of states || []) {
if (!Object.hasOwn(FEED_STATE_SEVERITY, state)) continue;
const rank = FEED_STATE_SEVERITY[state];
if (rank < worstRank) {
worst = state;
worstRank = rank;
}
}
return worst;
}
/**
* One honest layer snapshot for voice/HUD narration.
*
* Disabled layers report `feedState: 'off'` — the same reading the chips use —
* even if leftover stats would otherwise classify as stale. Counts and errors
* still travel so a disabled layer with a last-known error is inspectable.
*
* @param {object} [layer] `DataLayerManager.getAll()` projection, or a layer-like
* object with `id`, `enabled`, `stats`, and optional `source`/`name`.
* @param {{ now?: number }} [options]
* @returns {{
* id: string|null,
* name: string|null,
* enabled: boolean,
* feedState: string,
* source: string|null,
* count: number,
* lastUpdate: number|null,
* ageSec: number|null,
* ageLabel: string|null,
* error: string|null,
* }}
*/
export function layerSnapshot(layer = {}, { now = Date.now() } = {}) {
const stats = (layer && typeof layer.stats === 'object' && layer.stats) || {};
const source = String(stats.source || layer.source || '').trim() || null;
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 feedState = enabled ? layerFeedState({ ...stats, source }) : 'off';
return {
id: layer.id || layer.layerKey || null,
name: layer.name || layer.label || layer.id || layer.layerKey || null,
enabled,
feedState,
source,
count,
lastUpdate,
ageSec: snapshotAgeSec(lastUpdate, now),
ageLabel: snapshotAgeLabel(lastUpdate, now),
error: snapshotError(stats),
};
}
/**
* Snapshot every layer in a roster.
* @param {Array<object>} [layers]
* @param {{ now?: number }} [options]
* @returns {Array<object>}
*/
export function layerSnapshots(layers = [], options) {
if (!Array.isArray(layers)) return [];
return layers.map((layer) => layerSnapshot(layer, options));
}
function asSnapshot(entry, now) {
if (!entry || typeof entry !== 'object') return layerSnapshot({}, { now });
if (typeof entry.feedState === 'string') return entry;
return layerSnapshot(entry, { now });
}
/**
* Mechanical narration note so the model does not have to infer honesty.
* @param {Array<object>} layers Snapshots.
* @param {string|null} overall Worst considered feed-state.
* @returns {string}
*/
export function feedProvenanceNote(layers = [], overall = null) {
if (!overall) {
return 'No layer snapshot is attached. Do not invent a feed-state or a count.';
}
if (overall === 'nominal') {
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';
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 bits = flagged.map((s) => {
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})` : ''}`;
});
const body = bits.join('; ') || `${String(overall).toUpperCase()} feed`;
return `${body}. Narrate that provenance with any count; do not describe this as live.`;
}
/**
* Shared envelope stamped onto analyst, view-state, and HUD payloads.
* @param {Array<object>} [snapshots] Snapshots or raw `getAll()` rows.
* @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));
const active = layers.filter((s) => s.feedState && s.feedState !== 'off');
const overall = worstFeedState(active.map((s) => s.feedState))
|| (layers.length ? 'off' : null);
return {
overall,
layers,
note: feedProvenanceNote(layers, overall),
};
}
/**
* Compact voice-tool layer row: identity + the fields a model must read.
* @param {object} layer Raw `getAll()` row or snapshot.
* @param {{ now?: number }} [options]
* @returns {object}
*/
export function viewStateLayerRow(layer, options) {
const snap = layerSnapshot(layer, options);
return {
id: snap.id,
name: snap.name,
enabled: snap.enabled,
count: snap.count,
error: snap.error,
feedState: snap.feedState,
source: snap.source,
lastUpdate: snap.lastUpdate,
};
}
+182
View File
@@ -0,0 +1,182 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import {
FEED_STATE_SEVERITY,
LAYER_FEED_STATE_LABELS,
LAYER_FEED_STATES,
SNAPSHOT_FEED_STATES,
feedProvenanceEnvelope,
feedProvenanceNote,
layerFeedState,
layerSnapshot,
layerSnapshots,
snapshotAgeLabel,
snapshotAgeSec,
viewStateLayerRow,
worstFeedState,
} from './layerSnapshot.js';
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');
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({ 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');
});
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([...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.stale < FEED_STATE_SEVERITY.fallback, true);
assert.equal(worstFeedState(['nominal', 'stale', 'fallback']), 'stale');
assert.equal(worstFeedState(['degraded', 'unavailable']), 'unavailable');
assert.equal(worstFeedState([]), null);
});
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 });
assert.equal(snap.feedState, 'off');
assert.equal(snap.enabled, false);
assert.equal(snap.count, 12);
assert.equal(snap.ageLabel, '4m ago');
});
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',
},
}, { now: NOW });
assert.equal(snap.feedState, 'stale');
assert.equal(snap.source, 'OpenSky Network');
assert.equal(snap.ageSec, 240);
assert.equal(snap.ageLabel, '4m ago');
const envelope = feedProvenanceEnvelope([snap], { now: NOW });
assert.equal(envelope.overall, 'stale');
assert.match(envelope.note, /Live Flights is STALE/);
assert.match(envelope.note, /OpenSky Network/);
assert.match(envelope.note, /4m ago/);
assert.match(envelope.note, /do not describe this as live/i);
});
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 envelope = feedProvenanceEnvelope(layers, { now: NOW });
assert.equal(envelope.overall, 'degraded');
assert.match(envelope.note, /Active Fires is DEGRADED/);
assert.match(envelope.note, /FIRMS key missing/);
});
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 });
assert.equal(snap.feedState, 'unavailable');
const envelope = feedProvenanceEnvelope([snap]);
assert.equal(envelope.overall, 'unavailable');
assert.match(envelope.note, /UNAVAILABLE/);
});
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 });
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 });
assert.deepEqual(row, {
id: 'flights',
name: 'Live Flights',
enabled: true,
count: 9,
error: null,
feedState: 'nominal',
source: 'OpenSky Network',
lastUpdate: 42,
});
});
test('empty roster envelope does not pretend to be nominal', () => {
const envelope = feedProvenanceEnvelope([]);
assert.equal(envelope.overall, null);
assert.match(envelope.note, /Do not invent/);
});
test('age labels match the Data Layers meta clock', () => {
assert.equal(snapshotAgeSec(null, NOW), null);
assert.equal(snapshotAgeLabel(NOW - 3_000, NOW), 'just now');
assert.equal(snapshotAgeLabel(NOW - 12_000, NOW), '12s ago');
assert.equal(snapshotAgeLabel(NOW - 240_000, NOW), '4m ago');
assert.equal(snapshotAgeLabel(NOW - 7_200_000, NOW), '2h ago');
});
+7 -59
View File
@@ -1,5 +1,9 @@
import { governorRequestRender } from '../renderGovernor.js';
import { markDetectionSourcesChanged } from './detection.js';
import { LAYER_FEED_STATE_LABELS, layerFeedState } from './layerSnapshot.js';
export { layerFeedState };
function cloneLayerParams(value) {
if (Array.isArray(value)) return value.map(cloneLayerParams);
if (value && typeof value === 'object') {
@@ -10,15 +14,6 @@ function cloneLayerParams(value) {
return value;
}
const FEED_STATE_LABELS = Object.freeze({
nominal: 'ON',
loading: 'LOADING',
degraded: 'DEGRADED',
stale: 'STALE',
fallback: 'FALLBACK',
unavailable: 'UNAVAILABLE',
});
const SUPERSEDED_VISIBILITY_INTENT = Symbol('superseded-visibility-intent');
const VALID_LAYER_SERIALIZATION_DISPOSITIONS = new Set([
'enabled-only',
@@ -64,53 +59,6 @@ function refreshFailureFromStats(stats, label) {
return null;
}
/**
* Normalize heterogeneous layer stats into one honest control-chip state.
* @param {object|null} stats Layer getStats() result.
* @returns {'nominal'|'loading'|'degraded'|'stale'|'fallback'|'unavailable'} Feed state.
*/
export function layerFeedState(stats = {}) {
const state = stats || {};
const status = typeof state.status === 'string' ? state.status.toLowerCase() : '';
const source = `${state.source || ''} ${state.coverage || ''}`;
const hasExplicitFallback = typeof state.fallback === 'boolean';
const hasPriorData = Number(state.count) > 0 || Boolean(state.lastUpdate);
const presentedError = state.error || state.lastError || state.managerRefreshError;
if (['unavailable', 'offline', 'down', 'error'].includes(status)) return 'unavailable';
if (
(presentedError || state.unavailable === true || state.available === false)
&& !hasPriorData
&& !['zoom-in', 'empty', 'idle'].includes(status)
) {
return 'unavailable';
}
if (state.loading) return 'loading';
// Guidance states ask the user to act (zoom in, run a search) — normal
// operation, not feed faults. One honesty carve-out: layers keep their
// rendered records through the guidance state, so a genuinely stale cache
// still reads STALE; a guidance prompt alone never reads DEGRADED.
if (['zoom-in', 'empty', 'idle'].includes(status)) {
return state.stale ? 'stale' : 'nominal';
}
if (
state.fallback === true
|| status === 'fallback'
|| state.mode === 'sim'
|| /\bfallback\b/i.test(source)
|| (!hasExplicitFallback && /\badsb\.lol\b/i.test(source))
) {
return 'fallback';
}
if (state.stale || status === 'stale') return 'stale';
if (
state.degraded
|| presentedError
|| state.unavailable === true
|| state.available === false
) return 'degraded';
return 'nominal';
}
/**
* DataLayerManager — Manages registration, toggling, and update loops
* for real-time data overlays on the CesiumJS globe.
@@ -2217,7 +2165,7 @@ export class DataLayerManager {
_buildMetaText(layer) {
const stats = layer.stats || {};
const feedState = layerFeedState(stats);
const stateLabel = FEED_STATE_LABELS[feedState];
const stateLabel = LAYER_FEED_STATE_LABELS[feedState];
const source = stats.source || layer.source;
const lifecycleState = layer.lifecycleState || (layer.enabled ? 'enabled' : 'disabled');
if (lifecycleState === 'enabling' || lifecycleState === 'disabling') {
@@ -2267,7 +2215,7 @@ export class DataLayerManager {
button.classList.toggle('enabling', layer.lifecycleState === 'enabling');
button.classList.toggle('disabling', layer.lifecycleState === 'disabling');
button.classList.toggle('lifecycle-uncertain', uncertain);
for (const state of Object.keys(FEED_STATE_LABELS)) {
for (const state of Object.keys(LAYER_FEED_STATE_LABELS)) {
button.classList.toggle(`feed-${state}`, layer.enabled && !uncertain && feedState === state);
}
button.dataset.feedState = transitioning
@@ -2281,7 +2229,7 @@ export class DataLayerManager {
button.setAttribute('aria-busy', String(transitioning));
button.textContent = transitioning
? layer.lifecycleState.toUpperCase()
: (uncertain ? 'UNCERTAIN' : (layer.enabled ? FEED_STATE_LABELS[feedState] : 'OFF'));
: (uncertain ? 'UNCERTAIN' : (layer.enabled ? LAYER_FEED_STATE_LABELS[feedState] : 'OFF'));
button.setAttribute('aria-label', `${layer.name}: ${button.textContent}`);
}
+5 -7
View File
@@ -654,15 +654,13 @@ test('the voice TOOL SCHEMA is byte-identical to main — the mission mapping is
const end = src.indexOf('\n];\n', start);
const block = src.slice(start, end + 4);
// Re-pinned 2026-08-28: the Provider Settings / Esri release DELIBERATELY
// extends set_map_stack's enum with 'esri-imagery' (a real new basemap —
// exactly the kind of schema change this pin exists to make loud). The
// guarded claim is unchanged: first-run missions ride existing tools, and
// any NEW drift from this recorded schema still fails here.
assert.equal(block.length, 31189, 'tool schema byte length drifted from the pinned release schema');
// Re-pinned 2026-09-12: analyst_query and get_current_view_state descriptions
// now require feedProvenance narration. First-run missions still ride
// existing tools; any NEW drift from this recorded schema still fails here.
assert.equal(block.length, 31550, 'tool schema byte length drifted from the pinned release schema');
assert.equal(
crypto.createHash('sha256').update(block).digest('hex'),
'73aaabdb169a5478893d28688f327a21edd32ed3ec16fc6287bd944ed77beecf',
'350a7678155a97d0ded399dc2368db8596d520bed65c5b81bebce0ee3d9a76d4',
'the first-run missions must ride EXISTING tools: no schema edit, no cache bust',
);
+12 -8
View File
@@ -19,7 +19,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 { isHudSummaryUnconfigured } from './hudSummaryResponse.js';
import { isHudSummaryUnconfigured, hudSummaryLayerContext, hudTelemetryProvenanceTag } from './hudSummaryResponse.js';
/** Color palettes keyed by shader mode; applied as CSS custom properties. */
const HUD_COLORS = {
@@ -587,8 +587,9 @@ export class IntelHUD {
: 'N/A';
// NEAR the nearest catalogued POI at metro range; otherwise the lat/lon sector.
const localityTag = composeLocalityTag(nearest, m.latDeg, m.lonDeg);
return `${modeLabel} ${band} ${localityTag} | ${region} | ALT ${altTag} | WINDOW ${winTag} | SUN ${m.sunEl.toFixed(0)}° | ONA ${m.ona.toFixed(0)}° | ${localTag}`;
const provenance = hudTelemetryProvenanceTag(this._dataManager?.getAll?.() || []);
const line = `${modeLabel} ${band} ${localityTag} | ${region} | ALT ${altTag} | WINDOW ${winTag} | SUN ${m.sunEl.toFixed(0)}° | ONA ${m.ona.toFixed(0)}° | ${localTag}`;
return provenance ? `${line} | ${provenance}` : line;
}
/**
@@ -701,14 +702,11 @@ export class IntelHUD {
async _summaryContext() {
const labels = await getBasemapLabelContext(this.viewer);
const enabledLayers = this._dataManager?.getAll?.()
?.filter((layer) => layer.enabled)
.map((layer) => layer.name) || [];
return {
placeLabels: labels.placeLabels,
streetLabels: labels.streetLabels,
nearbyPlaceLabels: labels.nearbyPlaceLabels,
enabledLayerLabels: enabledLayers,
...hudSummaryLayerContext(this._dataManager?.getAll?.() || []),
};
}
@@ -838,7 +836,13 @@ export class IntelHUD {
this._dataManager = dataManager || null;
if (typeof this._dataManager?.subscribe === 'function') {
this._dataManagerUnsubscribe = this._dataManager.subscribe((change) => {
if (change?.type === 'visibility') this._markSummaryDirty();
if (
change?.type === 'visibility'
|| change?.type === 'refresh-transition'
|| change?.type === 'refresh-cancelled'
) {
this._markSummaryDirty();
}
});
}
this._markSummaryDirty();
+62
View File
@@ -1,3 +1,8 @@
import {
feedProvenanceEnvelope,
layerSnapshots,
} from './data/layerSnapshot.js';
export const HUD_SUMMARY_UNCONFIGURED_CODE = 'OPENAI_NOT_CONFIGURED';
/**
@@ -33,3 +38,60 @@ export function isHudSummaryUnconfigured(status, data) {
&& data?.error === null
&& data?.summary === null;
}
/**
* HUD summary model instructions. The five-word line may only describe
* supplied labels, and must carry feed-state when any enabled layer is not
* nominal.
*/
export const HUD_SUMMARY_INSTRUCTIONS = [
"Write one concise intelligence-HUD summary for God's Eye View.",
'Use only the supplied place, street, nearby-place, enabled-layer labels, and feedProvenance.',
'Prefer the clearest named place and include a relevant enabled layer only when useful.',
'If any enabled layer is not nominal, the five words MUST include that feedState token (STALE, DEGRADED, FALLBACK, LOADING, or UNAVAILABLE) and must not present the view as live.',
'Do not infer from coordinates or invent a place or feed state.',
'Output exactly five words with no title, punctuation, markdown, or introductory phrase.',
].join(' ');
/**
* Stamp HUD summary context with the same layer snapshots voice tools use.
* @param {Array<object>} [layers] `DataLayerManager.getAll()` rows.
* @param {{ now?: number }} [options]
* @returns {{
* enabledLayerLabels: string[],
* enabledLayers: Array<{id: string|null, name: string|null, feedState: string, source: string|null, count: number}>,
* feedProvenance: object,
* }}
*/
export function hudSummaryLayerContext(layers = [], options) {
const snapshots = layerSnapshots(layers, options).filter((s) => s.enabled);
const envelope = feedProvenanceEnvelope(snapshots, options);
return {
enabledLayerLabels: snapshots.map((s) => s.name).filter(Boolean),
enabledLayers: snapshots.map((s) => ({
id: s.id,
name: s.name,
feedState: s.feedState,
source: s.source,
count: s.count,
})),
feedProvenance: envelope,
};
}
/**
* Deterministic telemetry suffix when an enabled feed is not nominal.
* @param {Array<object>} [layers]
* @param {{ now?: number }} [options]
* @returns {string|null}
*/
export function hudTelemetryProvenanceTag(layers = [], options) {
const snapshots = layerSnapshots(layers, options).filter((s) => s.enabled);
const envelope = feedProvenanceEnvelope(snapshots, options);
if (!envelope.overall || envelope.overall === 'nominal') return null;
const names = envelope.layers
.filter((s) => s.feedState && s.feedState !== 'nominal' && s.feedState !== 'off')
.map((s) => String(s.name || s.id || 'LAYER').toUpperCase())
.slice(0, 2);
return `${envelope.overall.toUpperCase()}${names.length ? ` ${names.join('/')}` : ''}`;
}
+42
View File
@@ -1,7 +1,11 @@
import assert from 'node:assert/strict';
import test from 'node:test';
import { readFileSync } from 'node:fs';
import {
HUD_SUMMARY_INSTRUCTIONS,
HUD_SUMMARY_UNCONFIGURED_CODE,
hudSummaryLayerContext,
hudTelemetryProvenanceTag,
isHudSummaryUnconfigured,
keylessHudSummaryResponse,
} from './hudSummaryResponse.js';
@@ -128,3 +132,41 @@ test('the installed keyless HUD route stays successful after the voice quota is
else process.env.GEV_RATELIMIT_OPENAI_PER_MIN = previousLimit;
}
});
test('HUD summary context and telemetry tag carry chip-identical feed-state', () => {
const now = 1_000_000;
const layers = [
{
id: 'flights',
name: 'Live Flights',
enabled: true,
stats: { stale: true, count: 12, lastUpdate: now - 240_000, source: 'OpenSky Network' },
},
{
id: 'earthquakes',
name: 'Earthquakes',
enabled: false,
stats: { count: 3, lastUpdate: now },
},
];
const context = hudSummaryLayerContext(layers, { now });
assert.deepEqual(context.enabledLayerLabels, ['Live Flights']);
assert.equal(context.enabledLayers[0].feedState, 'stale');
assert.equal(context.feedProvenance.overall, 'stale');
assert.equal(hudTelemetryProvenanceTag(layers, { now }), 'STALE LIVE FLIGHTS');
assert.equal(hudTelemetryProvenanceTag([{
id: 'flights', name: 'Live Flights', enabled: true, stats: { count: 4, lastUpdate: now },
}], { now }), null);
});
test('HUD summary instructions require non-nominal feedState in the five words', () => {
assert.match(HUD_SUMMARY_INSTRUCTIONS, /five words MUST include that feedState token/);
assert.match(HUD_SUMMARY_INSTRUCTIONS, /STALE, DEGRADED, FALLBACK, LOADING, or UNAVAILABLE/);
assert.match(HUD_SUMMARY_INSTRUCTIONS, /feedProvenance/);
});
test('the HUD proxy uses the shared provenance instructions', () => {
const local = readFileSync(new URL('../server/providers/local.js', import.meta.url), 'utf8');
assert.match(local, /HUD_SUMMARY_INSTRUCTIONS/);
assert.doesNotMatch(local, /enabled-layer text labels/);
});
+27 -8
View File
@@ -64,6 +64,17 @@ test('the counting contract is stated in the Realtime instructions', () => {
assert.match(text, /flights layer loads where you look/, 'rule 5: the loaded-data caveat');
});
test('the feed-state contract is stated in the Realtime instructions', () => {
const start = voice.indexOf("'FEED STATE CONTRACT");
assert.ok(start >= 0, 'the feed-state contract instruction is missing');
const text = voice.slice(start, voice.indexOf('\n', start));
assert.match(text, /feedProvenance/, 'names the envelope');
assert.match(text, /layerFeedState/, 'same classifier as the chips');
assert.match(text, /nominal\/loading\/degraded\/stale\/fallback\/unavailable/);
assert.match(text, /NEVER narrate a count as live/);
assert.match(text, /Confirm only what the tool result returned/);
});
test('Context panel opening stays distinct from Contacts activation', () => {
const start = voice.indexOf("'For requests to open, show, reveal, or focus a menu/panel");
assert.ok(start >= 0, 'panel-routing instruction is missing');
@@ -106,6 +117,10 @@ test('nearest-aircraft selection stays out of Contacts and Cockpit', () => {
assert.match(nearest.description, /exclude on-ground records/);
assert.match(nearest.description, /fallback feeds remain usable/);
assert.match(nearest.description, /does not open Contacts or Cockpit/);
const analyst = byName.get('analyst_query');
assert.match(analyst.description, /feedProvenance/);
assert.match(analyst.description, /never describe a stale/);
});
test('the two Context/Cockpit tools pin their enums and required arguments', () => {
@@ -145,9 +160,12 @@ test('the edited existing tools changed exactly as intended', () => {
);
assert.deepEqual(panel.parameters.required, ['panelId', 'open']);
// Edit 2: description only — the view state now reports Context and Cockpit.
// Edit 2: description only — the view state now reports Context and Cockpit
// plus honest per-layer feedState / feedProvenance.
const viewState = byName.get('get_current_view_state');
assert.match(viewState.description, /Context, Cockpit/);
assert.match(viewState.description, /feedState/);
assert.match(viewState.description, /feedProvenance/);
assert.deepEqual(viewState.parameters.properties, {});
// Edit 3: dependent multi-tool navigation can wait for the destination view.
@@ -157,11 +175,11 @@ test('the edited existing tools changed exactly as intended', () => {
});
test('no unchanged Realtime tool definition drifts silently', () => {
// Context/Cockpit parity, the dependent-location wait edit, and the retired
// `bing-road` stack leaving `set_map_stack`'s enum are the known schema
// changes. Everything else must be byte-identical: an unnoticed edit
// to a shipped tool changes
// model behavior in production with nothing in review to catch it.
// Context/Cockpit parity, the dependent-location wait edit, the retired
// `bing-road` stack leaving `set_map_stack`'s enum, and analyst_query
// feedProvenance copy are the known schema changes. Everything else must be
// byte-identical: an unnoticed edit to a shipped tool changes model behavior
// in production with nothing in review to catch it.
//
// If this fails and the change was deliberate, re-derive the digest and say
// in the mic-test brief which tools moved — the session cache busts on any
@@ -174,16 +192,17 @@ test('no unchanged Realtime tool definition drifts silently', () => {
'fly_to_location',
'select_nearest_aircraft',
'set_map_stack',
'analyst_query',
]);
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, 20);
const digest = createHash('sha256')
.update(JSON.stringify(unchanged))
.digest('hex')
.slice(0, 16);
assert.equal(digest, '802ed694b8887b88', 'an unchanged Realtime tool definition drifted');
assert.equal(digest, '5db68d7c29f6ed4f', 'an unchanged Realtime tool definition drifted');
});
test('Radio volume and mission speed share the Sharpen slider visual language', () => {
+92 -15
View File
@@ -9,7 +9,12 @@ import { getNextIssPass } from '../data/satellites.js';
import { CCTV_FOCUS_RESULT } from '../data/cctv.js';
import { contextModeWord } from '../contextModePolicy.js';
import { createAnalystEngine } from '../data/analystEngine.js';
import { layerFeedState } from '../data/manager.js';
import {
feedProvenanceEnvelope,
layerSnapshot,
layerSnapshots,
viewStateLayerRow,
} from '../data/layerSnapshot.js';
import militaryAwarenessLayer, {
collectAircraftProximityWindow,
contactsWindowFromSnapshot,
@@ -503,10 +508,17 @@ export function createGevActionRunner({ viewer, styleManager, dataManager, scene
}
const stats = layerModule?.getStats?.() || {};
const source = String(stats.source || layerModule?.source || '').trim() || null;
const feed = {
state: layerFeedState({ ...stats, source }),
const snap = layerSnapshot({
id: layerId,
name: layer.label || layerId,
enabled: true,
source,
count: Number.isFinite(Number(stats.count)) ? Number(stats.count) : null,
stats: { ...stats, source },
});
const feed = {
state: snap.feedState,
source: snap.source,
count: snap.count,
};
const nearest = await createAnalystEngine(analystProviders(viewer, dataManager, {
@@ -2356,6 +2368,8 @@ function normalizeLocationId(value) {
function getCurrentViewState(viewer, styleManager, dataManager, sceneDirector = null) {
const cartographic = Cesium.Cartographic.fromCartesian(viewer.camera.positionWC);
const layerRows = (dataManager?.getAll?.() || []).map((layer) => viewStateLayerRow(layer));
const enabledSnapshots = layerSnapshots(dataManager?.getAll?.() || []).filter((s) => s.enabled);
return {
ok: true,
action: 'get_current_view_state',
@@ -2379,13 +2393,8 @@ function getCurrentViewState(viewer, styleManager, dataManager, sceneDirector =
controls: typeof styleManager.getControlState === 'function' ? styleManager.getControlState() : null,
scenePlayback: sceneDirector?.getPlaybackStatus?.() || null,
tracked: collectTrackedEntities(dataManager),
layers: dataManager.getAll().map((layer) => ({
id: layer.id,
name: layer.name,
enabled: layer.enabled,
count: layer.stats?.count || 0,
error: layer.stats?.error || null,
})),
layers: layerRows,
feedProvenance: feedProvenanceEnvelope(enabledSnapshots),
};
}
@@ -2482,6 +2491,7 @@ function aircraftProximityWindowForQuery(args, result) {
scope: `window:${radiusKm}km@${label}`,
followUp: false,
note: 'Contacts window engine — the same computation and cohort the Contacts panel displays, so this count matches the panel exactly — counts cover loaded data; the flights layer loads by viewport.',
...(result?.coverage?.feedProvenance ? { feedProvenance: result.coverage.feedProvenance } : {}),
},
// (D) The answer always says whose window it is and which engine produced it.
window: {
@@ -2493,6 +2503,12 @@ function aircraftProximityWindowForQuery(args, result) {
aircraft: window.aircraft,
},
...(activeContactsWindow() ? { contactsWindow: activeContactsWindow() } : {}),
...(result?.coverage?.feedProvenance
? {
feedProvenance: result.coverage.feedProvenance,
feedState: result.coverage.feedProvenance.overall,
}
: {}),
};
}
@@ -3310,6 +3326,47 @@ function activeContactsWindow() {
}
}
function provenanceForQuery(dataManager, layerKeys) {
const keys = Array.isArray(layerKeys) && layerKeys.length ? layerKeys : ['flights'];
const all = dataManager?.getAll?.() || [];
const snapshots = keys.map((key) => {
const row = all.find((layer) => layer.id === key);
if (row) return layerSnapshot(row);
const enabled = Boolean(dataManager?.isEnabled?.(key));
return layerSnapshot({ id: key, enabled });
});
return feedProvenanceEnvelope(snapshots);
}
function attachQueryProvenance(result, dataManager, args = {}) {
if (!result || typeof result !== 'object') return result;
const keys = Array.isArray(args.layers) && args.layers.length
? args.layers
: (result.coverage?.layersQueried || []).map((row) => row.layerKey);
const envelope = provenanceForQuery(dataManager, keys);
if (result.coverage) {
result.coverage.feedProvenance = envelope;
if (Array.isArray(result.coverage.layersQueried)) {
const byId = new Map(envelope.layers.map((snap) => [snap.id, snap]));
result.coverage.layersQueried = result.coverage.layersQueried.map((row) => {
const snap = byId.get(row.layerKey);
if (!snap) return row;
return {
...row,
feedState: snap.feedState,
source: snap.source,
lastUpdate: snap.lastUpdate,
enabled: snap.enabled,
error: snap.error,
};
});
}
}
result.feedProvenance = envelope;
result.feedState = envelope.overall;
return result;
}
function analystProviders(viewer, dataManager, { recordLimitByLayer = null } = {}) {
return {
getRecords(layerKey) {
@@ -3322,6 +3379,20 @@ function analystProviders(viewer, dataManager, { recordLimitByLayer = null } = {
? (mod.getAnalystRecords(requestedLimit) || [])
: (mod.getAnalystRecords() || []);
},
getLayerSnapshot(layerKey) {
const row = (dataManager.getAll?.() || []).find((layer) => layer.id === layerKey);
if (row) return layerSnapshot(row);
const enabled = Boolean(dataManager.isEnabled?.(layerKey));
const entry = dataManager.layers?.get?.(layerKey);
const stats = entry?.module?.getStats?.() || {};
return layerSnapshot({
id: layerKey,
name: entry?.module?.name || layerKey,
enabled,
source: stats.source || entry?.module?.source || null,
stats,
});
},
resolveRegionRing: (name) => resolveRegionRingForQuery(name),
/**
* The active Contacts subject, when there is one — the centre the operator
@@ -3367,7 +3438,13 @@ async function runAnalystQuery(viewer, dataManager, args = {}) {
limit: args.limit,
followUp: Boolean(args.followUp),
});
if (!result.ok) return { ok: false, action: 'analyst_query', error: result.error, coverage: result.coverage };
if (!result.ok) {
return attachQueryProvenance(
{ ok: false, action: 'analyst_query', error: result.error, coverage: result.coverage },
dataManager,
args,
);
}
// Compact payload for the voice model: identity + the fields queries sort/
// filter on. The full record set stays engine-side for follow-ups.
//
@@ -3411,7 +3488,7 @@ async function runAnalystQuery(viewer, dataManager, args = {}) {
// arbitrary points — only "how many aircraft around <this contact>" is
// unified, because that is the question the panel is already answering.
const entityWindow = aircraftProximityWindowForQuery(args, result);
if (entityWindow) return entityWindow;
if (entityWindow) return attachQueryProvenance(entityWindow, dataManager, args);
const contactsWindow = activeContactsWindow();
const aircraftQueried = (result.coverage?.layersQueried || [])
@@ -3432,7 +3509,7 @@ async function runAnalystQuery(viewer, dataManager, args = {}) {
+ `"how many aircraft are nearby". This query measured something else — ${result.count} ${result.scopeLabel}. `
+ 'Give this one only if the operator asked about that specific area, and name both scopes if you give both.'
: null;
return {
return attachQueryProvenance({
ok: true,
action: 'analyst_query',
count: result.count,
@@ -3455,5 +3532,5 @@ async function runAnalystQuery(viewer, dataManager, args = {}) {
}
: {}),
...(countsReconciliation ? { countsReconciliation } : {}),
};
}, dataManager, args);
}
+66
View File
@@ -732,6 +732,9 @@ test('Data Layers voice inventory hides the Context coordinator while current-vi
assert.deepEqual(menu.layers.map(({ id }) => id), ['flights']);
const current = await runner('get_current_view_state');
assert.deepEqual(current.layers.map(({ id }) => id), ['flights', 'military-awareness']);
assert.equal(current.layers.find(({ id }) => id === 'flights').feedState, 'off');
assert.equal(current.layers.find(({ id }) => id === 'military-awareness').feedState, 'nominal');
assert.equal(current.feedProvenance.overall, 'nominal');
// The Contacts mode's internal id is 'flights'; the tools accept 'contacts'.
// State output reports the accepted word so the model cannot read its own
// active context as "off", with the internal id kept for layer reasoning.
@@ -3020,3 +3023,66 @@ test('front5: 0.99 km due EAST is the subject, though a degree box rejects it',
assert.equal(result.window.centeredOn, 'N546PC');
});
});
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);
});