fix: derive HUD caption feed states from section results

The caption request declared every answered section nominal, so a stale
aircraft snapshot reached the caption as live. Each section now reports
stale or unavailable from its own result, and the provenance envelope is
built with the app's feedProvenanceEnvelope.
This commit is contained in:
Sameh Khamis committed 2026-10-02 15:21:36 -07:00
1 parent 853c60583f
commit cac5ba3b72
3 files changed
+71 -15

No files matched your search

+5 -1
View File
@@ -3127,7 +3127,11 @@
"src/layers/transit/policy.js",
"src/data/transitFeeds.js",
"src/layers/transit/movement.js",
"src/data/contactPlayback.js"
"src/data/contactPlayback.js",
"src/data/layerSnapshot.js",
"src/data/feedState.js",
"src/loadingFeedback.js",
"src/data/installationFeedback.js"
],
"external": ["satellite.js"]
},
+25 -14
View File
@@ -1,5 +1,6 @@
/** Composite queries that combine other tools' answers for one area. */
import { feedProvenanceEnvelope } from '../../data/layerSnapshot.js';
import { defineTool, ToolError } from '../catalog.js';
import {
AREA_SCHEMA,
@@ -83,6 +84,15 @@ const AWARENESS_SECTIONS = [
},
];
/** A section's feed state for the caption, from what its result reported. */
function sectionFeedState(section) {
if (section.unavailable) return 'unavailable';
const data = section.data || {};
return data.freshness === 'stale' || data.stale === true
? 'stale'
: 'nominal';
}
/** The resolved area, plus the argument sections receive in its place. */
function sectionArea(resolved) {
return {
@@ -179,16 +189,16 @@ export const getHudCaption = defineTool({
requires: ['weather', 'summary'],
async run(args, context) {
const { area, center, brief, lines } = await buildBrief(args, context);
// The app's caption context: place labels and the layers that answered,
// with their feed state.
const answered = SECTIONS.filter(
({ key }) => brief[key] && !brief[key].unavailable,
// The app's caption context: place labels and each section as a layer
// with the feed state its result reported, failures included.
const layers = SECTIONS.filter(({ key }) => brief[key]).map(
({ key, label }) => ({
id: key,
name: label,
enabled: true,
feedState: sectionFeedState(brief[key]),
}),
);
const layers = answered.map(({ key, label }) => ({
id: key,
name: label,
feedState: 'nominal',
}));
const response = await context.services.summary.summarize(
{
placeLabels: [area.label],
@@ -196,11 +206,12 @@ export const getHudCaption = defineTool({
center,
observations: lines,
enabledLayerLabels: layers.map((layer) => layer.name),
enabledLayers: layers,
feedProvenance: {
overall: layers.length ? 'nominal' : null,
layers,
},
enabledLayers: layers.map(({ id, name, feedState }) => ({
id,
name,
feedState,
})),
feedProvenance: feedProvenanceEnvelope(layers),
},
{ signal: context.signal },
);
+41
View File
@@ -601,3 +601,44 @@ test('composites reach their sections through the catalog', async () => {
['get_weather', 'situation_brief'],
]);
});
test('the HUD caption context carries each section feed state', async () => {
const sent = [];
const summary = {
async summarize(context) {
sent.push(context);
return {
ok: true,
status: 200,
data: { summary: 'Austin aircraft STALE' },
};
},
};
const aircraft = {
getSnapshot: async () => ({
records: [],
complete: true,
source: 'Test feed',
freshness: 'stale',
}),
};
const cyclones = {
getSnapshot: async () => {
throw new Error('upstream down');
},
};
await catalogWith({ weather, summary, places, aircraft, cyclones }).call(
'get_hud_caption',
{ area: { place: 'Austin' } },
);
assert.deepEqual(
sent[0].enabledLayers.map((layer) => [layer.id, layer.feedState]),
[
['weather', 'nominal'],
['aircraft', 'stale'],
['cyclones', 'unavailable'],
],
);
assert.equal(sent[0].feedProvenance.overall, 'unavailable');
assert.match(sent[0].feedProvenance.note, /Aircraft is STALE/);
});