From 603f428397f61ddd06893b3178081398f5f9c50c Mon Sep 17 00:00:00 2001 From: quanticsoul4772 <71096528+quanticsoul4772@users.noreply.github.com> Date: Sat, 10 Oct 2026 20:35:24 -0700 Subject: [PATCH] fix(tools): say when a breakdown's field is missing from the feed OpenSky, the default civil feed, reports no operator or type code, so a breakdown by either put every aircraft under unknown while the summary said nothing. The summary now says "no operator reported" in that case, and otherwise appends the unknown count after the largest groups. Co-Authored-By: Claude Opus 5.5 --- docs/CURRENT-STATE.md | 5 +++-- src/tools/queries/aviation.js | 4 +++- src/tools/queries/aviation.test.mjs | 2 +- src/tools/queries/maritime.test.mjs | 2 +- src/tools/results.js | 13 ++++++++++--- src/tools/results.test.mjs | 12 +++++++++++- 6 files changed, 29 insertions(+), 9 deletions(-) diff --git a/docs/CURRENT-STATE.md b/docs/CURRENT-STATE.md index 520bb9207..f9e9c02a7 100644 --- a/docs/CURRENT-STATE.md +++ b/docs/CURRENT-STATE.md @@ -96,8 +96,9 @@ shared `area` argument (place name, bounding box, or point and radius) and cap lists at 25 rows by default. `aircraft_in_area` and `vessels_in_area` can also count every match by a field (`group_by`: operator, country or type for aircraft; type or destination for ships; the ten largest groups, then other and -unknown counts) and summarize airborne altitude and speed (`stats`: count, min, -median, mean, max). Both cover all matches, not just the rows returned. Voice offers the same queries next to its app +unknown counts; the summary says when a feed reports none of the field) and +summarize airborne altitude and speed (`stats`: count, min, median, mean, max). +Both cover all matches, not just the rows returned. Voice offers the same queries next to its app actions: the session lists them, and the browser runs them through the same catalog, loaded on first use. See [tools and the MCP server](TOOLS.md). diff --git a/src/tools/queries/aviation.js b/src/tools/queries/aviation.js index e3aca82c4..23652f211 100644 --- a/src/tools/queries/aviation.js +++ b/src/tools/queries/aviation.js @@ -105,7 +105,9 @@ export const aircraftInArea = defineTool({ group_by: { type: 'string', enum: Object.keys(GROUP_LABELS), - description: 'Count matched aircraft by this field, largest first.', + description: + 'Count matched aircraft by this field, largest first. Feeds that do not ' + + 'report the field count those aircraft as unknown.', }, stats: { type: 'boolean', diff --git a/src/tools/queries/aviation.test.mjs b/src/tools/queries/aviation.test.mjs index 618085b00..b6b6e3497 100644 --- a/src/tools/queries/aviation.test.mjs +++ b/src/tools/queries/aviation.test.mjs @@ -409,7 +409,7 @@ test('aircraft in an area can be counted by operator over every match', async () }); assert.equal( result.summary, - '4 aircraft in 60 km around 37.600, -122.300; most by operator: United (2), Delta (1).', + '4 aircraft in 60 km around 37.600, -122.300; most by operator: United (2), Delta (1); 1 unknown.', ); assert.equal(result.data.returned, 1); assert.deepEqual(result.data.breakdown, { diff --git a/src/tools/queries/maritime.test.mjs b/src/tools/queries/maritime.test.mjs index d1c43e60a..662e5bdf2 100644 --- a/src/tools/queries/maritime.test.mjs +++ b/src/tools/queries/maritime.test.mjs @@ -237,7 +237,7 @@ test('vessels in an area can be counted by destination with a speed summary', as }); assert.equal( result.summary, - '2 vessels in 20 km around 37.780, -122.400; most by destination: OAKLAND (1).', + '2 vessels in 20 km around 37.780, -122.400; most by destination: OAKLAND (1); 1 unknown.', ); assert.deepEqual(result.data.breakdown, { field: 'destination', diff --git a/src/tools/results.js b/src/tools/results.js index b4a24c72a..528f59ea5 100644 --- a/src/tools/results.js +++ b/src/tools/results.js @@ -78,13 +78,20 @@ export function numericSummary(rows, field) { }; } -/** "; most by operator: Delta (12), United (9), American (7)" or "". */ +/** + * "; most by operator: Delta (12), United (9), American (7); 4 unknown", or + * "; no operator reported" when no row carries the field. + */ export function breakdownPhrase(result, label) { - if (!result.groups.length) return ''; + if (!result.groups.length) + return result.unknown_count ? `; no ${label} reported` : ''; const top = result.groups .slice(0, 3) .map((group) => `${group.value} (${group.count})`); - return `; most by ${label}: ${top.join(', ')}`; + const unknown = result.unknown_count + ? `; ${result.unknown_count} unknown` + : ''; + return `; most by ${label}: ${top.join(', ')}${unknown}`; } function sum(values) { diff --git a/src/tools/results.test.mjs b/src/tools/results.test.mjs index ba67b9f64..2e480afcd 100644 --- a/src/tools/results.test.mjs +++ b/src/tools/results.test.mjs @@ -63,6 +63,7 @@ test('a numeric summary skips missing values', () => { test('the breakdown phrase names the three largest groups', () => { const result = { + unknown_count: 0, groups: [ { value: 'A', count: 4 }, { value: 'B', count: 3 }, @@ -74,5 +75,14 @@ test('the breakdown phrase names the three largest groups', () => { breakdownPhrase(result, 'type'), '; most by type: A (4), B (3), C (2)', ); - assert.equal(breakdownPhrase({ groups: [] }, 'type'), ''); + assert.equal( + breakdownPhrase({ ...result, unknown_count: 5 }, 'type'), + '; most by type: A (4), B (3), C (2); 5 unknown', + ); + // A feed that never reports the field says so instead of staying silent. + assert.equal( + breakdownPhrase({ groups: [], unknown_count: 236 }, 'operator'), + '; no operator reported', + ); + assert.equal(breakdownPhrase({ groups: [], unknown_count: 0 }, 'type'), ''); });