feat(tools): count aircraft in an area by airline

OpenSky, the default civil feed, reports no operator, so group_by: operator
said "no operator reported". aircraft_in_area now also takes
group_by: 'airline', read from the ICAO designator in each callsign: three
letters followed by a flight number that starts with a digit (SWA1234,
UAL12A). Registrations used as callsigns (N123AB) count as unknown. Rows are
unchanged; the designator is derived for the breakdown only.

Live, Texas: "785 aircraft ...; most by airline: AAL (89), SWA (65), UAL (52);
283 unknown."

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
quanticsoul4772andClaude Opus 5.5 committed 2026-10-17 14:32:46 -07:00
1 parent db73c6908e
commit aec4f67ca9
5 files changed
+80 -13

No files matched your search

+6 -4
View File
@@ -1,6 +1,8 @@
- `aircraft_in_area` and `vessels_in_area` take `group_by` and `stats`, so an
MCP client can ask which countries, aircraft types or ship types dominate an
area, or the average altitude or speed, over every match instead of
estimating from the returned rows. Operator and type are counted only where
the feed reports them; AIS ship type 0 ("not available") counts as unknown
MCP client can ask which airlines, countries, aircraft types or ship types
dominate an area, or the average altitude or speed, over every match instead
of estimating from the returned rows. Airlines come from the ICAO designator
in each callsign (SWA1234 counts as SWA), so the breakdown works on feeds
that report no operator; operator and type are counted only where the feed
reports them; AIS ship type 0 ("not available") counts as unknown
(quanticsoul4772, #1056).
+5 -4
View File
@@ -98,10 +98,11 @@ combined situation brief, military awareness around a point, the app's heads-up
opens the app over an area. Tools reuse the layers' portable source factories, take a
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; the summary says when a feed reports none of the field) and
summarize airborne altitude and speed (`stats`: count, min, median, mean, max).
count every match by a field (`group_by`: airline, read from the ICAO
designator in the callsign, or operator, country or type for aircraft; type or
destination for ships; the ten largest groups, then other and 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 leaves these two
tools out, so they reach MCP clients only. Voice offers the same queries next to its app
actions: the session lists them, and the browser runs them through the same
+23 -2
View File
@@ -36,11 +36,29 @@ const MILITARY_SCHEMA = Object.freeze({
});
const MAX_TRACK_POINTS = 200;
const GROUP_LABELS = Object.freeze({
airline: 'airline',
operator: 'operator',
origin_country: 'country',
type_code: 'type',
});
/**
* The ICAO airline designator a callsign flies under: three letters, then a
* flight number that starts with a digit (SWA1234, UAL12A, BAW1). Registrations
* used as callsigns (N123AB, GABCD) have no designator.
*/
export function airlineDesignator(callsign) {
const match = /^([A-Z]{3})\d[0-9A-Z]{0,4}$/.exec(
String(callsign ?? '')
.trim()
.toUpperCase(),
);
return match ? match[1] : null;
}
const groupValue = (field) =>
field === 'airline' ? (row) => airlineDesignator(row.callsign) : undefined;
const round = (value, digits) =>
Number.isFinite(value) ? Number(value.toFixed(digits)) : null;
@@ -91,7 +109,8 @@ export const aircraftInArea = defineTool({
description:
'Aircraft currently reported inside an area, nearest the center first. ' +
'Optionally military aircraft only, airborne only, or within an altitude band. ' +
'group_by counts every matched aircraft by operator, country or type, and ' +
'group_by counts every matched aircraft by airline (the ICAO designator ' +
'in its callsign), operator, country or type, and ' +
'stats summarizes airborne altitude and speed; both cover all matches, not ' +
'just the rows returned.',
inputSchema: {
@@ -151,7 +170,9 @@ export const aircraftInArea = defineTool({
...(regional ? [`regional feed: ${snapshot.coverage}`] : []),
...(snapshot.freshness === 'stale' ? ['data may be stale'] : []),
];
const groups = args.group_by ? breakdown(rows, args.group_by) : null;
const groups = args.group_by
? breakdown(rows, args.group_by, groupValue(args.group_by))
: null;
return {
summary:
`${rows.length} ${kind} in ${area.label}` +
+41
View File
@@ -1,6 +1,7 @@
import assert from 'node:assert/strict';
import test from 'node:test';
import { composeCatalog, coreTools } from '../index.js';
import { airlineDesignator } from './aviation.js';
import { LiveSourceError } from '../../sources/live/contract.js';
const record = (id, latitude, longitude, extra = {}) => ({
@@ -504,3 +505,43 @@ test('altitude bands require a reported altitude from normalized OpenSky records
});
assert.equal(minimum.data.total, 3);
});
test('an airline designator is read from an ICAO flight callsign only', () => {
for (const [callsign, designator] of [
['SWA1234', 'SWA'],
['ual12a', 'UAL'],
[' BAW1 ', 'BAW'],
['RCH123', 'RCH'],
['N123AB', null],
['GABCD', null],
['SWA', null],
['SWAX123', null],
['', null],
[null, null],
])
assert.equal(airlineDesignator(callsign), designator, String(callsign));
});
test('aircraft in an area can be counted by airline from their callsigns', async () => {
const fleet = [
record('a1', 37.6, -122.3, { callsign: 'SWA100' }),
record('a2', 37.61, -122.3, { callsign: 'SWA2' }),
record('a3', 37.62, -122.3, { callsign: 'UAL9' }),
record('a4', 37.63, -122.3, { callsign: 'N123AB' }),
];
const catalog = composeCatalog({
tools: coreTools,
services: { aircraft: feed(fleet) },
});
const result = await catalog.call('aircraft_in_area', {
area: bay,
group_by: 'airline',
});
assert.equal(
result.summary,
'4 aircraft in 60 km around 37.600, -122.300; most by airline: SWA (2), UAL (1); 1 unknown.',
);
assert.equal(result.data.breakdown.field, 'airline');
// Rows are unchanged: the designator is derived, not added to each row.
assert.equal('airline' in result.data.rows[0], false);
});
+5 -3
View File
@@ -28,13 +28,15 @@ export const MAX_GROUPS = 10;
/**
* Count rows by one text field over every row, not just those returned.
* Values group case-insensitively and keep their first spelling; rows
* without a value are counted as unknown.
* without a value are counted as unknown. `valueOf` derives the value when
* it is not a row field as it stands.
*/
export function breakdown(rows, field) {
export function breakdown(rows, field, valueOf = (row) => row[field]) {
const groups = new Map();
let unknown = 0;
for (const row of rows) {
const value = typeof row[field] === 'string' ? row[field].trim() : '';
const raw = valueOf(row);
const value = typeof raw === 'string' ? raw.trim() : '';
if (!value) {
unknown += 1;
continue;