mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-05 10:53:59 +00:00
fix(voice): keep canonical analyst IDs out of display-text normalization
compactAnalystItem normalized every string field, including `id`. Bikeshare uses the raw station name as its ID, so a station with repeated spaces or a name over 180 code units was registered as a referent under a different key and a numbered fly_to_location reported it as no longer loaded. Normalize display fields only; layerKey, id, icao24, mmsi, noradId, registration and callsign pass through as the layer reports them. Adds an analyst_query -> referent -> fly_to_location test for repeated-space and long station names, and documents the rule in docs/CURRENT-STATE.md. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
445af0875b
commit
cbca6a228d
3 files changed
+65
-3
No files matched your search
@@ -13,8 +13,12 @@ External ALPR tags, surveillance-tool display fields, voice context text and
|
|||||||
compact analyst strings collapse controls and whitespace before a final
|
compact analyst strings collapse controls and whitespace before a final
|
||||||
180-character cap (scene labels retain their 120-character cap). Empty ALPR
|
180-character cap (scene labels retain their 120-character cap). Empty ALPR
|
||||||
values remain null and Unicode text is preserved without splitting a surrogate
|
values remain null and Unicode text is preserved without splitting a surrogate
|
||||||
pair. These fields remain untrusted source data in structured tool output; text
|
pair. Only display fields are normalized: canonical analyst identifiers
|
||||||
normalization does not establish trust or eliminate prompt injection.
|
(`id`, `layerKey`, `icao24`, `mmsi`, `noradId`, `registration`, `callsign`)
|
||||||
|
pass through unchanged, because bikeshare uses the raw station name as its ID
|
||||||
|
and numbered voice references resolve by that key. These fields remain
|
||||||
|
untrusted source data in structured tool output; text normalization does not
|
||||||
|
establish trust or eliminate prompt injection.
|
||||||
|
|
||||||
Terrain height requests accept at most 64 points, matching the browser service's
|
Terrain height requests accept at most 64 points, matching the browser service's
|
||||||
existing batch size and one bounded upstream chunk. Larger requests return HTTP
|
existing batch size and one bounded upstream chunk. Larger requests return HTTP
|
||||||
|
|||||||
+19
-1
@@ -4570,6 +4570,21 @@ const ANALYST_IDENTITY_FIELDS = Object.freeze([
|
|||||||
'noradId',
|
'noradId',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Analyst item keys that identify a record rather than describe it: the
|
||||||
|
* referent registry, track_entity and locateLayerEntity match on them, so
|
||||||
|
* display-text normalization must leave them byte-for-byte unchanged.
|
||||||
|
*/
|
||||||
|
const ANALYST_CANONICAL_FIELDS = new Set([
|
||||||
|
'layerKey',
|
||||||
|
'id',
|
||||||
|
'icao24',
|
||||||
|
'mmsi',
|
||||||
|
'noradId',
|
||||||
|
'registration',
|
||||||
|
'callsign',
|
||||||
|
]);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Compact one analyst record for the voice payload: identity, position (so a
|
* Compact one analyst record for the voice payload: identity, position (so a
|
||||||
* follow-up can fly to it), distance, and that layer's manifest fields with
|
* follow-up can fly to it), distance, and that layer's manifest fields with
|
||||||
@@ -4607,8 +4622,11 @@ function compactAnalystItem(r) {
|
|||||||
} else compact[key] = r[key];
|
} else compact[key] = r[key];
|
||||||
}
|
}
|
||||||
if (present(r.ageHours)) compact.ageHours = r.ageHours;
|
if (present(r.ageHours)) compact.ageHours = r.ageHours;
|
||||||
|
// Display text only: canonical keys are lookup identities (bikeshare uses
|
||||||
|
// the raw station name as its id), so the referent registry and
|
||||||
|
// locateLayerEntity must see them exactly as the layer reports them.
|
||||||
for (const key of Object.keys(compact)) {
|
for (const key of Object.keys(compact)) {
|
||||||
if (typeof compact[key] === 'string')
|
if (typeof compact[key] === 'string' && !ANALYST_CANONICAL_FIELDS.has(key))
|
||||||
compact[key] = normalizeDisplayText(compact[key]);
|
compact[key] = normalizeDisplayText(compact[key]);
|
||||||
}
|
}
|
||||||
return compact;
|
return compact;
|
||||||
|
|||||||
@@ -3944,3 +3944,43 @@ test('analyst item display strings are single-line and bounded for voice', async
|
|||||||
assert.equal(result.items[0].id, 'stable-id');
|
assert.equal(result.items[0].id, 'stable-id');
|
||||||
for (const value of Object.values(result.items[0])) if (typeof value === 'string') assert.doesNotMatch(value, /[\r\n\t]/);
|
for (const value of Object.values(result.items[0])) if (typeof value === 'string') assert.doesNotMatch(value, /[\r\n\t]/);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('analyst_query keeps canonical IDs raw so numbered references still fly to the station', async () => {
|
||||||
|
globalThis.window = globalThis.window || { clearTimeout, setTimeout, requestIdleCallback: null };
|
||||||
|
const { viewer, styleManager } = createVoiceNavigationHarness();
|
||||||
|
const referents = createReferentRegistry();
|
||||||
|
const longName = 'Station ' + 'é'.repeat(200);
|
||||||
|
const spaced = 'Congress Ave & 2nd';
|
||||||
|
const stations = [
|
||||||
|
{ id: spaced, name: spaced, lat: 30.2672, lon: -97.7431 },
|
||||||
|
{ id: longName, name: longName, lat: 30.2711, lon: -97.7437 },
|
||||||
|
];
|
||||||
|
const bikeshare = { getStats: () => ({ count: stations.length, lastUpdate: Date.now() }), getAnalystRecords: () => stations };
|
||||||
|
const runner = createActionRunner({
|
||||||
|
viewer, styleManager,
|
||||||
|
dataManager: {
|
||||||
|
layers: new Map([['bikeshare', { module: bikeshare }]]),
|
||||||
|
isEnabled: () => true,
|
||||||
|
getAll: () => [{ id: 'bikeshare', name: 'bikeshare', enabled: true, stats: bikeshare.getStats() }],
|
||||||
|
},
|
||||||
|
deixis: { referents },
|
||||||
|
});
|
||||||
|
const listed = await runner('analyst_query', { layers: ['bikeshare'], scope: { kind: 'anywhere' }, limit: 2 });
|
||||||
|
assert.equal(listed.ok, true);
|
||||||
|
const bySpaced = listed.items.find((item) => item.id === spaced);
|
||||||
|
const byLong = listed.items.find((item) => item.id === longName);
|
||||||
|
assert.ok(bySpaced && byLong, 'compact items keep the raw station-name IDs');
|
||||||
|
assert.equal(bySpaced.name, 'Congress Ave & 2nd', 'display name is still normalized');
|
||||||
|
assert.equal(byLong.name.length, 180, 'display name is still bounded');
|
||||||
|
assert.equal(byLong.id.length, longName.length);
|
||||||
|
const registered = referents.list();
|
||||||
|
assert.equal(registered.length, 2, 'the analyst result became the numbered list');
|
||||||
|
for (const ref of registered) {
|
||||||
|
const station = stations.find((row) => row.id === ref.id);
|
||||||
|
assert.ok(station, `referent ${ref.n} keeps the canonical ID`);
|
||||||
|
const flown = await runner('fly_to_location', { referent: ref.n });
|
||||||
|
assert.equal(flown.ok, true, `referent ${ref.n} resolves instead of "no longer loaded"`);
|
||||||
|
assert.equal(flown.latitude, station.lat);
|
||||||
|
assert.equal(flown.longitude, station.lon);
|
||||||
|
}
|
||||||
|
});
|
||||||
Reference in new issue
Block a user