fix(gnss): separate GEV integrity classifier from GPSJAM aggregation; machine-readable provenance

Review on #855: the per-aircraft rule (NIC < 7 || NACp < 8, borrowed from
the US 14 CFR 91.227 ADS-B Out minima) is GEV-defined, not gpsjam's, and
gpsjam's map shows low navigation accuracy, not necessarily jamming.

- Primary cell state is now `navigation-integrity-anomaly` (medium/high) or
  `nominal`; `suspected-gnss-interference` is a secondary `interpretation`
  marked validated: false.
- GNSS_PROVENANCE / gnssProvenance({ windowMs }) records the classifier
  (gev-nic-nacp-v1, GEV, unvalidated), the gpsjam aggregation (formula,
  bands [0.02, 0.1], FAQ source) and the GEV 30-minute visited-view window,
  built from the constants the code applies. Exposed as layer.provenance
  and getStats().provenance; the proxy tags responses with `classifier`.
- Visible name "GNSS Integrity", legend "... low accuracy", blurb leads with
  low navigation accuracy and states the threshold is a GEV choice.
- Layer id `gnss-interference` and share token `0` unchanged.
- Voice aliases add "gnss integrity", "navigation integrity",
  "low navigation accuracy"; the interference aliases stay.
- Docs describe the method as GEV threshold + gpsjam aggregation.
- Tests: provenance object, renamed state/label, proxy classifier tag;
  qa-gnss asserts provenance and wording in the real app.

Hex project - Crew Leo Agile dev team.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
This commit is contained in:
Leo PechnickiandClaude Opus 5.5 authored and Mazeyar Moeini Feizabadi committed 2026-10-13 17:20:43 +02:00
1 parent 40b06a24d4
commit 6d4a2a02fe
16 files changed
+325 -67

No files matched your search

+40 -5
View File
@@ -1,6 +1,7 @@
#!/usr/bin/env node
/**
* qa-gnss.mjs — headless proof for the GNSS Interference layer.
* qa-gnss.mjs — headless proof for the GNSS Integrity layer
* (id `gnss-interference`): low navigation accuracy reported by ADS-B.
*
* Drives the REAL app in headless Chromium against a dev server (keyless).
* The live section reads adsb.lol through `/api/gnss-integrity` (60 s server
@@ -16,6 +17,8 @@
* with 1 degraded is low (first bad discounted), a clean cell at sea
* is low, a 2-aircraft cell is withheld; `stale: true` reaches
* getStats. Every band is drawn and low is shown over land and sea.
* The row reads as low navigation accuracy, and stats carry the
* machine-readable provenance (GEV classifier, gpsjam bands, window).
* (iii) FAILURE — the route stubbed to 502: stats.error is set and the
* cells already on the globe are kept.
* (iv) DISABLE — the layer's data source is hidden.
@@ -108,13 +111,19 @@ async function enableAndSettle(page, { timeoutS = 30, refresh = false } = {}) {
if (stats.count > 0 || stats.error) break;
await new Promise((r) => setTimeout(r, 1000));
}
const legend = layer
.getRowControls()
.legend.map(({ label, count }) => ({ label, count }));
const rowControls = layer.getRowControls().legend;
const legend = rowControls.map(({ label, count }) => ({ label, count }));
const blurb = rowControls[0]?.blurb ?? '';
const rowName =
document.querySelector(`[data-layer-id="${id}"] .data-name`)
?.textContent ?? '';
const ds = window.__godsEyeView.viewer.dataSources.getByName(id)[0];
return {
stats,
legend,
blurb,
rowName,
name: layer.name,
entities: ds?.entities.values.length ?? -1,
shown: ds?.show ?? null,
};
@@ -217,7 +226,7 @@ function fixturePayload() {
}
async function main() {
console.log('\nGNSS Interference proof (qa-gnss)');
console.log('\nGNSS Integrity proof (qa-gnss)');
console.log(` App URL : ${APP_URL}\n`);
try {
const res = await fetch(APP_URL);
@@ -346,6 +355,32 @@ async function main() {
band('Over') === 1 && band('2') === 1 && band('Under') === 2,
JSON.stringify(f.legend),
);
const prov = f.stats.provenance;
record(
'PROVENANCE: GEV classifier, gpsjam bands and visited-view window are machine-readable',
prov?.classifier?.id === 'gev-nic-nacp-v1' &&
prov.classifier.definedBy === 'GEV' &&
prov.classifier.validated === false &&
prov.aggregation?.source === 'https://gpsjam.org/faq' &&
JSON.stringify(prov.aggregation.bands) === '[0.02,0.1]' &&
prov.window?.minutes === 30 &&
prov.window.scope === 'visited-view' &&
prov.interpretation?.validated === false,
JSON.stringify(prov?.classifier),
);
record(
'WORDING: the row leads with low navigation accuracy, interference only as the hedged reading',
f.name === 'GNSS Integrity' &&
f.rowName.includes('GNSS Integrity') &&
!/interference/i.test(f.rowName) &&
f.legend.every(({ label }) => label.endsWith('low accuracy')) &&
f.blurb.startsWith(
'Share of ADS-B aircraft reporting low navigation accuracy',
) &&
/GEV threshold/.test(f.blurb) &&
/Suspected jamming or spoofing is one possible cause/.test(f.blurb),
`name=${JSON.stringify(f.name)}`,
);
record(
'BANDS: stale snapshot is reported',
f.stats.stale === true,