@motionstudies/core 0.1.0-alpha.25 → 0.1.0-alpha.26
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/air-enrichment.d.ts +63 -0
- package/air-enrichment.js +76 -0
- package/package.json +6 -1
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { type AirContinent } from './air-continents.ts';
|
|
2
|
+
import type { AirSearchTrack } from './air-search.ts';
|
|
3
|
+
export type EndpointSide = 'origin' | 'destination';
|
|
4
|
+
export type EndpointEvidence = 'observed' | 'corroborated' | 'candidate' | 'conflicting' | 'unknown';
|
|
5
|
+
export interface EnrichmentAirport {
|
|
6
|
+
icao: string;
|
|
7
|
+
iata: string;
|
|
8
|
+
name: string;
|
|
9
|
+
continent: AirContinent | null;
|
|
10
|
+
}
|
|
11
|
+
export interface EnrichmentSource {
|
|
12
|
+
id: string;
|
|
13
|
+
kind: 'airport-reference' | 'route-reference' | 'association-audit';
|
|
14
|
+
url: string;
|
|
15
|
+
sha256: string;
|
|
16
|
+
license: string;
|
|
17
|
+
}
|
|
18
|
+
export interface EndpointProposal {
|
|
19
|
+
trackId: string;
|
|
20
|
+
callsign: string;
|
|
21
|
+
aircraft: string;
|
|
22
|
+
side: EndpointSide;
|
|
23
|
+
airport: string;
|
|
24
|
+
anchor: string;
|
|
25
|
+
status: 'corroborated' | 'candidate' | 'conflicting';
|
|
26
|
+
sourceIds: string[];
|
|
27
|
+
reasons: string[];
|
|
28
|
+
}
|
|
29
|
+
/** Sidecar metadata only. Proposed endpoints deliberately have no playback or event time. */
|
|
30
|
+
export interface AirEnrichment {
|
|
31
|
+
kind: 'air-endpoint-enrichment';
|
|
32
|
+
schemaVersion: 1;
|
|
33
|
+
date: string;
|
|
34
|
+
sourceManifestSha256: string;
|
|
35
|
+
airports: EnrichmentAirport[];
|
|
36
|
+
sources: EnrichmentSource[];
|
|
37
|
+
proposals: EndpointProposal[];
|
|
38
|
+
}
|
|
39
|
+
export interface EndpointLabel {
|
|
40
|
+
status: EndpointEvidence;
|
|
41
|
+
airport?: EnrichmentAirport;
|
|
42
|
+
sourceIds: readonly string[];
|
|
43
|
+
reasons: readonly string[];
|
|
44
|
+
}
|
|
45
|
+
/** Reject wrong-day, foreign-track, duplicate, stale-anchor and observed-overwrite metadata. */
|
|
46
|
+
export declare function readAirEnrichment(value: unknown, binding: {
|
|
47
|
+
date: string;
|
|
48
|
+
sourceManifestSha256: string;
|
|
49
|
+
tracks: readonly AirSearchTrack[];
|
|
50
|
+
}): AirEnrichment;
|
|
51
|
+
export declare function createEndpointResolver(enrichment: AirEnrichment): (track: AirSearchTrack, side: EndpointSide) => EndpointLabel;
|
|
52
|
+
export declare function usableEndpoint(label: EndpointLabel, includeCandidates?: boolean): boolean;
|
|
53
|
+
export declare function endpointCoverage(tracks: readonly AirSearchTrack[], resolve: ReturnType<typeof createEndpointResolver>, side: EndpointSide, includeCandidates?: boolean): {
|
|
54
|
+
observed: number;
|
|
55
|
+
corroborated: number;
|
|
56
|
+
candidate: number;
|
|
57
|
+
conflicting: number;
|
|
58
|
+
unknown: number;
|
|
59
|
+
segments: number;
|
|
60
|
+
usable: number;
|
|
61
|
+
unresolved: number;
|
|
62
|
+
continentKnown: number;
|
|
63
|
+
};
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { isAirContinent } from './air-continents.js';
|
|
2
|
+
const object = (v) => !!v && typeof v === 'object' && !Array.isArray(v);
|
|
3
|
+
const text = (v) => typeof v === 'string' && v.length > 0;
|
|
4
|
+
const hash = (v) => typeof v === 'string' && /^[a-f0-9]{64}$/.test(v);
|
|
5
|
+
const side = (v) => v === 'origin' || v === 'destination';
|
|
6
|
+
/** Reject wrong-day, foreign-track, duplicate, stale-anchor and observed-overwrite metadata. */
|
|
7
|
+
export function readAirEnrichment(value, binding) {
|
|
8
|
+
const fail = () => { throw Error('Invalid or mismatched endpoint enrichment'); };
|
|
9
|
+
if (!object(value) || value.kind !== 'air-endpoint-enrichment' || value.schemaVersion !== 1 || value.date !== binding.date
|
|
10
|
+
|| !hash(value.sourceManifestSha256) || value.sourceManifestSha256 !== binding.sourceManifestSha256
|
|
11
|
+
|| !Array.isArray(value.airports) || !Array.isArray(value.sources) || !Array.isArray(value.proposals))
|
|
12
|
+
return fail();
|
|
13
|
+
const airports = new Set(), sources = new Map(), proposals = new Set();
|
|
14
|
+
for (const a of value.airports) {
|
|
15
|
+
if (!object(a) || !text(a.icao) || typeof a.iata !== 'string' || !text(a.name) || (a.continent !== null && !isAirContinent(a.continent)) || airports.has(a.icao))
|
|
16
|
+
return fail();
|
|
17
|
+
airports.add(a.icao);
|
|
18
|
+
}
|
|
19
|
+
for (const s of value.sources) {
|
|
20
|
+
if (!object(s) || !text(s.id) || sources.has(s.id) || !text(s.url) || !hash(s.sha256) || !text(s.license)
|
|
21
|
+
|| !['airport-reference', 'route-reference', 'association-audit'].includes(String(s.kind)))
|
|
22
|
+
return fail();
|
|
23
|
+
// Links are either public HTTPS sources or simple sibling report files.
|
|
24
|
+
if (!/^https:\/\/[^\s]+$/.test(s.url) && !/^reports\/[a-zA-Z0-9._-]+\.json$/.test(s.url))
|
|
25
|
+
return fail();
|
|
26
|
+
sources.set(s.id, String(s.kind));
|
|
27
|
+
}
|
|
28
|
+
const tracks = new Map(binding.tracks.map(t => [t.id, t]));
|
|
29
|
+
for (const p of value.proposals) {
|
|
30
|
+
if (!object(p) || !text(p.trackId) || !side(p.side) || !text(p.airport) || !airports.has(p.airport)
|
|
31
|
+
|| !['corroborated', 'candidate', 'conflicting'].includes(String(p.status)) || !Array.isArray(p.sourceIds) || !p.sourceIds.length
|
|
32
|
+
|| !p.sourceIds.every(s => typeof s === 'string' && sources.has(s)) || !Array.isArray(p.reasons) || !p.reasons.every(text))
|
|
33
|
+
return fail();
|
|
34
|
+
const t = tracks.get(p.trackId), key = `${p.trackId}:${p.side}`, opposite = p.side === 'origin' ? 'destination' : 'origin';
|
|
35
|
+
if (!t || t[p.side] || t[opposite]?.icao !== p.anchor || t.callsign !== p.callsign || t.icaoAddress !== p.aircraft || proposals.has(key))
|
|
36
|
+
return fail();
|
|
37
|
+
if (!p.sourceIds.some(s => sources.get(s) === 'route-reference')
|
|
38
|
+
|| (p.status !== 'candidate' && !p.sourceIds.some(s => sources.get(s) === 'association-audit'))
|
|
39
|
+
|| (p.status === 'corroborated' && p.reasons.length))
|
|
40
|
+
return fail();
|
|
41
|
+
proposals.add(key);
|
|
42
|
+
}
|
|
43
|
+
return value;
|
|
44
|
+
}
|
|
45
|
+
export function createEndpointResolver(enrichment) {
|
|
46
|
+
const airports = new Map(enrichment.airports.map(a => [a.icao, a]));
|
|
47
|
+
const proposals = new Map(enrichment.proposals.map(p => [`${p.trackId}:${p.side}`, p]));
|
|
48
|
+
const referenceIds = enrichment.sources.filter(s => s.kind === 'airport-reference').map(s => s.id);
|
|
49
|
+
return (track, side) => {
|
|
50
|
+
const observed = track[side];
|
|
51
|
+
if (observed)
|
|
52
|
+
return { status: 'observed', airport: { icao: observed.icao, iata: observed.iata, name: observed.name,
|
|
53
|
+
continent: isAirContinent(observed.continent) ? observed.continent : airports.get(observed.icao)?.continent ?? null }, sourceIds: referenceIds, reasons: [] };
|
|
54
|
+
const p = proposals.get(`${track.id}:${side}`);
|
|
55
|
+
if (!p)
|
|
56
|
+
return { status: 'unknown', sourceIds: [], reasons: [] };
|
|
57
|
+
return { status: p.status, airport: airports.get(p.airport), sourceIds: p.sourceIds, reasons: p.reasons };
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
export function usableEndpoint(label, includeCandidates = false) {
|
|
61
|
+
return label.status === 'observed' || label.status === 'corroborated' || (includeCandidates && label.status === 'candidate');
|
|
62
|
+
}
|
|
63
|
+
export function endpointCoverage(tracks, resolve, side, includeCandidates = false) {
|
|
64
|
+
const counts = { observed: 0, corroborated: 0, candidate: 0, conflicting: 0, unknown: 0 };
|
|
65
|
+
let usable = 0, continentKnown = 0;
|
|
66
|
+
for (const t of tracks) {
|
|
67
|
+
const label = resolve(t, side);
|
|
68
|
+
counts[label.status]++;
|
|
69
|
+
if (usableEndpoint(label, includeCandidates)) {
|
|
70
|
+
usable++;
|
|
71
|
+
if (label.airport?.continent)
|
|
72
|
+
continentKnown++;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return { segments: tracks.length, ...counts, usable, unresolved: tracks.length - usable, continentKnown };
|
|
76
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@motionstudies/core",
|
|
3
|
-
"version": "0.1.0-alpha.
|
|
3
|
+
"version": "0.1.0-alpha.26",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "Transport contracts and motion primitives for Motion Studies.",
|
|
@@ -229,6 +229,11 @@
|
|
|
229
229
|
"types": "./map-labels.d.ts",
|
|
230
230
|
"import": "./map-labels.js",
|
|
231
231
|
"default": "./map-labels.js"
|
|
232
|
+
},
|
|
233
|
+
"./air-enrichment": {
|
|
234
|
+
"types": "./air-enrichment.d.ts",
|
|
235
|
+
"import": "./air-enrichment.js",
|
|
236
|
+
"default": "./air-enrichment.js"
|
|
232
237
|
}
|
|
233
238
|
},
|
|
234
239
|
"files": [
|