mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-04 10:24:02 +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
|
||||
180-character cap (scene labels retain their 120-character cap). Empty ALPR
|
||||
values remain null and Unicode text is preserved without splitting a surrogate
|
||||
pair. These fields remain untrusted source data in structured tool output; text
|
||||
normalization does not establish trust or eliminate prompt injection.
|
||||
pair. Only display fields are normalized: canonical analyst identifiers
|
||||
(`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
|
||||
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',
|
||||
]);
|
||||
|
||||
/**
|
||||
* 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
|
||||
* 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];
|
||||
}
|
||||
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)) {
|
||||
if (typeof compact[key] === 'string')
|
||||
if (typeof compact[key] === 'string' && !ANALYST_CANONICAL_FIELDS.has(key))
|
||||
compact[key] = normalizeDisplayText(compact[key]);
|
||||
}
|
||||
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');
|
||||
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