From cac5ba3b722460e48df7dd08d35e5831f1554667 Mon Sep 17 00:00:00 2001 From: Sameh Khamis Date: Thu, 1 Oct 2026 14:54:02 -0700 Subject: [PATCH] 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. --- scripts/package-boundaries.json | 6 +++- src/tools/queries/brief.js | 39 +++++++++++++++--------- src/tools/queries/environment.test.mjs | 41 ++++++++++++++++++++++++++ 3 files changed, 71 insertions(+), 15 deletions(-) diff --git a/scripts/package-boundaries.json b/scripts/package-boundaries.json index 9c768aa65..7da7e6c08 100644 --- a/scripts/package-boundaries.json +++ b/scripts/package-boundaries.json @@ -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"] }, diff --git a/src/tools/queries/brief.js b/src/tools/queries/brief.js index 4997dd02d..750cd1dfb 100644 --- a/src/tools/queries/brief.js +++ b/src/tools/queries/brief.js @@ -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 }, ); diff --git a/src/tools/queries/environment.test.mjs b/src/tools/queries/environment.test.mjs index d1df866c9..b79f578d3 100644 --- a/src/tools/queries/environment.test.mjs +++ b/src/tools/queries/environment.test.mjs @@ -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/); +});