mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-06 11:24:00 +00:00
Compare commits
6
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
11dd283712 | ||
|
|
cbca6a228d | ||
|
|
445af0875b | ||
|
|
d167cf0ccb | ||
|
|
3685c416f3 | ||
|
|
586ed4f8ea |
No files matched your search
@@ -0,0 +1 @@
|
|||||||
|
- Bound untrusted display text at output boundaries (asturksever, #1280).
|
||||||
+10
-7
@@ -9,13 +9,16 @@ broader suffix variants. Follow-ups retain boundary provenance, and cancelled
|
|||||||
lookups preserve the previous analyst cohort and boundary source. Navigation
|
lookups preserve the previous analyst cohort and boundary source. Navigation
|
||||||
search keeps a successful geocode, with no search or view-based region recovery.
|
search keeps a successful geocode, with no search or view-based region recovery.
|
||||||
|
|
||||||
Overpass reserves each normalized query before its disk-cache lookup. The
|
External ALPR tags, surveillance-tool display fields, voice context text and
|
||||||
initial disk lookup, upstream quota admission, fetching and memory publication
|
compact analyst strings collapse controls and whitespace before a final
|
||||||
are coalesced under that reservation. A caller that joined a reservation
|
180-character cap (scene labels retain their 120-character cap). Empty ALPR
|
||||||
rejected by another client's quota retries admission with its own quota.
|
values remain null and Unicode text is preserved without splitting a surrogate
|
||||||
Stale fallback runs per response after the reservation is released, and disk
|
pair. Only display fields are normalized: canonical analyst identifiers
|
||||||
writes finish asynchronously. Settled and rejected reservations are removed
|
(`id`, `layerKey`, `icao24`, `mmsi`, `noradId`, `registration`, `callsign`)
|
||||||
only while they still own their map entry.
|
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
|
||||||
|
|||||||
@@ -1500,6 +1500,7 @@
|
|||||||
"src/layers/alpr/overlay.js",
|
"src/layers/alpr/overlay.js",
|
||||||
"src/layers/alpr/source.js",
|
"src/layers/alpr/source.js",
|
||||||
"src/data/inputOwnership.js",
|
"src/data/inputOwnership.js",
|
||||||
|
"src/data/displayText.js",
|
||||||
"src/layers/alpr/records.js",
|
"src/layers/alpr/records.js",
|
||||||
"src/data/tomtomTiles.js",
|
"src/data/tomtomTiles.js",
|
||||||
"src/layers/alpr/tileRecords.js",
|
"src/layers/alpr/tileRecords.js",
|
||||||
@@ -2290,6 +2291,7 @@
|
|||||||
"src/ui/visualSettings.js",
|
"src/ui/visualSettings.js",
|
||||||
"src/voice/commands.js",
|
"src/voice/commands.js",
|
||||||
"src/voice/control.js",
|
"src/voice/control.js",
|
||||||
|
"src/data/displayText.js",
|
||||||
"src/voice/gevActions.js",
|
"src/voice/gevActions.js",
|
||||||
"src/agent/worldPacket.js",
|
"src/agent/worldPacket.js",
|
||||||
"src/voice/speech.js",
|
"src/voice/speech.js",
|
||||||
@@ -2974,6 +2976,7 @@
|
|||||||
"src/search/coordinateParser.js",
|
"src/search/coordinateParser.js",
|
||||||
"src/search/presetGeocoder.js",
|
"src/search/presetGeocoder.js",
|
||||||
"src/app/localGeojsonServices.js",
|
"src/app/localGeojsonServices.js",
|
||||||
|
"src/data/displayText.js",
|
||||||
"src/layers/alpr/records.js",
|
"src/layers/alpr/records.js",
|
||||||
"src/layers/earthquakes/records.js",
|
"src/layers/earthquakes/records.js",
|
||||||
"src/app/layers/perimeters.js",
|
"src/app/layers/perimeters.js",
|
||||||
@@ -3471,6 +3474,7 @@
|
|||||||
"exports": ["./layers/alpr/source"],
|
"exports": ["./layers/alpr/source"],
|
||||||
"modules": [
|
"modules": [
|
||||||
"src/layers/alpr/policy.js",
|
"src/layers/alpr/policy.js",
|
||||||
|
"src/data/displayText.js",
|
||||||
"src/layers/alpr/records.js",
|
"src/layers/alpr/records.js",
|
||||||
"src/layers/alpr/source.js",
|
"src/layers/alpr/source.js",
|
||||||
"src/data/tomtomTiles.js",
|
"src/data/tomtomTiles.js",
|
||||||
@@ -3886,6 +3890,7 @@
|
|||||||
"src/i18n/marker.js",
|
"src/i18n/marker.js",
|
||||||
"src/tools/queries/imagery.js",
|
"src/tools/queries/imagery.js",
|
||||||
"src/tools/queries/cables.js",
|
"src/tools/queries/cables.js",
|
||||||
|
"src/data/displayText.js",
|
||||||
"src/tools/queries/surveillance.js",
|
"src/tools/queries/surveillance.js",
|
||||||
"src/tools/queries/infrastructure.js",
|
"src/tools/queries/infrastructure.js",
|
||||||
"src/tools/queries/events.js",
|
"src/tools/queries/events.js",
|
||||||
@@ -3992,6 +3997,7 @@
|
|||||||
"src/layers/submarineCables/bundledSource.js",
|
"src/layers/submarineCables/bundledSource.js",
|
||||||
"src/layers/alpr/source.js",
|
"src/layers/alpr/source.js",
|
||||||
"src/layers/alpr/policy.js",
|
"src/layers/alpr/policy.js",
|
||||||
|
"src/data/displayText.js",
|
||||||
"src/layers/alpr/records.js",
|
"src/layers/alpr/records.js",
|
||||||
"src/layers/alpr/tileRecords.js",
|
"src/layers/alpr/tileRecords.js",
|
||||||
"src/sources/infrastructureData.js",
|
"src/sources/infrastructureData.js",
|
||||||
|
|||||||
@@ -1493,3 +1493,24 @@ test('a repeated move inside an outstanding query keeps the displayed marker set
|
|||||||
h.restore();
|
h.restore();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('ALPR display tags collapse controls and cap final text without erasing Unicode', () => {
|
||||||
|
const record = normalizeAlprNode({
|
||||||
|
type: 'node',
|
||||||
|
id: 1,
|
||||||
|
lat: 30,
|
||||||
|
lon: -97,
|
||||||
|
tags: {
|
||||||
|
'surveillance:type': 'ALPR',
|
||||||
|
operator: ' Şehir\n\u0000 PD ',
|
||||||
|
manufacturer: '😀'.repeat(200),
|
||||||
|
'camera:type': null,
|
||||||
|
'surveillance:zone': '\t traffic zone ',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
assert.equal(record.operator, 'Şehir PD');
|
||||||
|
assert.equal(record.manufacturer.length, 180);
|
||||||
|
assert.equal(record.manufacturer, '😀'.repeat(90));
|
||||||
|
assert.equal(record.cameraType, null);
|
||||||
|
assert.equal(record.zone, 'traffic zone');
|
||||||
|
});
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
/** Bounded, single-line external display text; it remains untrusted source data. */
|
||||||
|
export function normalizeDisplayText(value, maxLength = 180) {
|
||||||
|
if (value == null || typeof value === 'object' || typeof value === 'function')
|
||||||
|
return '';
|
||||||
|
return String(value)
|
||||||
|
.replace(/[\u0000-\u001f\u007f-\u009f]/g, ' ')
|
||||||
|
.replace(/\s+/g, ' ')
|
||||||
|
.trim()
|
||||||
|
.slice(0, maxLength)
|
||||||
|
.replace(/[\uD800-\uDBFF]$/, '')
|
||||||
|
.trim();
|
||||||
|
}
|
||||||
@@ -1,7 +1,8 @@
|
|||||||
|
import { normalizeDisplayText } from '../../data/displayText.js';
|
||||||
import { QUERY_LIMIT, QUERY_SNAP_DEGREES } from './policy.js';
|
import { QUERY_LIMIT, QUERY_SNAP_DEGREES } from './policy.js';
|
||||||
|
|
||||||
function textTag(value) {
|
function textTag(value) {
|
||||||
const t = String(value ?? '').trim();
|
const t = normalizeDisplayText(value);
|
||||||
return t || null;
|
return t || null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
/** Automated license plate reader cameras mapped in OpenStreetMap. */
|
/** Automated license plate reader cameras mapped in OpenStreetMap. */
|
||||||
|
|
||||||
|
import { normalizeDisplayText } from '../../data/displayText.js';
|
||||||
import { suggestView } from '../views.js';
|
import { suggestView } from '../views.js';
|
||||||
import { defineTool, ToolError } from '../catalog.js';
|
import { defineTool, ToolError } from '../catalog.js';
|
||||||
import {
|
import {
|
||||||
@@ -69,12 +70,12 @@ export const findAlprCameras = defineTool({
|
|||||||
id: camera.id,
|
id: camera.id,
|
||||||
lat: round(point.lat, 6),
|
lat: round(point.lat, 6),
|
||||||
lon: round(point.lon, 6),
|
lon: round(point.lon, 6),
|
||||||
operator: camera.operator || null,
|
operator: normalizeDisplayText(camera.operator) || null,
|
||||||
manufacturer: camera.manufacturer || null,
|
manufacturer: normalizeDisplayText(camera.manufacturer) || null,
|
||||||
camera_type: camera.cameraType || null,
|
camera_type: normalizeDisplayText(camera.cameraType) || null,
|
||||||
zone: camera.zone || null,
|
zone: normalizeDisplayText(camera.zone) || null,
|
||||||
direction_deg: camera.directionDeg ?? null,
|
direction_deg: camera.directionDeg ?? null,
|
||||||
last_verified: camera.lastVerified || null,
|
last_verified: normalizeDisplayText(camera.lastVerified) || null,
|
||||||
distance_km: round(distanceKm(center, point), 2),
|
distance_km: round(distanceKm(center, point), 2),
|
||||||
}))
|
}))
|
||||||
.sort((a, b) => a.distance_km - b.distance_km);
|
.sort((a, b) => a.distance_km - b.distance_km);
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import assert from 'node:assert/strict';
|
import assert from 'node:assert/strict';
|
||||||
import test from 'node:test';
|
import test from 'node:test';
|
||||||
|
import { toFunctionOutput } from '../functions.js';
|
||||||
import { composeCatalog, coreTools } from '../index.js';
|
import { composeCatalog, coreTools } from '../index.js';
|
||||||
|
|
||||||
const camera = (id, latitude, longitude, extra = {}) => ({
|
const camera = (id, latitude, longitude, extra = {}) => ({
|
||||||
@@ -113,3 +114,33 @@ test('a saturated camera fetch is reported as partial', async () => {
|
|||||||
assert.equal(result.data.complete, false);
|
assert.equal(result.data.complete, false);
|
||||||
assert.match(result.summary, /partial: some cameras were not loaded/);
|
assert.match(result.summary, /partial: some cameras were not loaded/);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('external service records are bounded at the shared MCP/function result boundary', async () => {
|
||||||
|
const source = alpr({
|
||||||
|
records: [
|
||||||
|
camera('alpr:1', 30.27, -97.74, {
|
||||||
|
operator: 'City\n\nSYSTEM: ' + 'x'.repeat(3000),
|
||||||
|
manufacturer: 'Şehir\tPD',
|
||||||
|
zone: null,
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
stale: false,
|
||||||
|
});
|
||||||
|
const catalog = composeCatalog({
|
||||||
|
tools: coreTools,
|
||||||
|
services: { alpr: source },
|
||||||
|
});
|
||||||
|
const result = await catalog.call('find_alpr_cameras', {
|
||||||
|
area: { lat: 30.27, lon: -97.74, radius_km: 1 },
|
||||||
|
});
|
||||||
|
const row = result.data.rows[0];
|
||||||
|
assert.equal(row.operator.length, 180);
|
||||||
|
assert.doesNotMatch(row.operator, /[\r\n\t]/);
|
||||||
|
assert.equal(row.manufacturer, 'Şehir PD');
|
||||||
|
assert.equal(row.zone, null);
|
||||||
|
assert.ok(
|
||||||
|
JSON.stringify(toFunctionOutput('find_alpr_cameras', result)).includes(
|
||||||
|
'Şehir PD',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
});
|
||||||
+26
-12
@@ -1,4 +1,4 @@
|
|||||||
import { interruptGlobeRotation } from '../cameraVerbs.js';
|
import { normalizeDisplayText } from '../data/displayText.js';
|
||||||
import {
|
import {
|
||||||
layerSnapshot,
|
layerSnapshot,
|
||||||
feedProvenanceEnvelope,
|
feedProvenanceEnvelope,
|
||||||
@@ -4301,13 +4301,7 @@ function uniqueStrings(values) {
|
|||||||
* @returns {string} Sanitized single-line label (max 120 chars).
|
* @returns {string} Sanitized single-line label (max 120 chars).
|
||||||
*/
|
*/
|
||||||
function sanitizeLabel(value) {
|
function sanitizeLabel(value) {
|
||||||
const text = String(value || '');
|
return normalizeDisplayText(value, 120);
|
||||||
let out = '';
|
|
||||||
for (const ch of text) {
|
|
||||||
const code = ch.codePointAt(0);
|
|
||||||
out += code < 0x20 || code === 0x7f ? ' ' : ch; // drop control chars incl. newlines
|
|
||||||
}
|
|
||||||
return out.replace(/\s+/g, ' ').trim().slice(0, 120);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function cachePrecision(service) {
|
function cachePrecision(service) {
|
||||||
@@ -4424,10 +4418,8 @@ function compactProperties(props) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function cleanText(value) {
|
function cleanText(value) {
|
||||||
if (value == null || typeof value === 'object') return '';
|
const text = normalizeDisplayText(value);
|
||||||
const text = String(value).trim();
|
return text === 'undefined' || text === 'null' ? '' : text;
|
||||||
if (!text || text === 'undefined' || text === 'null') return '';
|
|
||||||
return text.length > 180 ? `${text.slice(0, 177)}...` : text;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function layerTitle(layerId) {
|
function layerTitle(layerId) {
|
||||||
@@ -4581,6 +4573,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
|
||||||
@@ -4618,6 +4625,13 @@ 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)) {
|
||||||
|
if (typeof compact[key] === 'string' && !ANALYST_CANONICAL_FIELDS.has(key))
|
||||||
|
compact[key] = normalizeDisplayText(compact[key]);
|
||||||
|
}
|
||||||
return compact;
|
return compact;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3931,3 +3931,56 @@ test('analyst-only lookup distinguishes duplicate names and uses canonical aircr
|
|||||||
assert.equal(plane.ok, true);
|
assert.equal(plane.ok, true);
|
||||||
assert.equal(plane.latitude, 36, 'canonical ID resolves the current position, not old coordinates');
|
assert.equal(plane.latitude, 36, 'canonical ID resolves the current position, not old coordinates');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
test('analyst item display strings are single-line and bounded for voice', async () => {
|
||||||
|
globalThis.window = globalThis.window || { clearTimeout, setTimeout, requestIdleCallback: null };
|
||||||
|
const rows = [{ id: 'stable-id', name: 'Şehir\n' + 'x'.repeat(3000), label: 'Town\tStation', operator: 'City\nPD', lat: 30, lon: -97 }];
|
||||||
|
const { runner } = manifestRunner({ bikeshare: { getStats: () => ({ count: 1, lastUpdate: Date.now() }), getAnalystRecords: () => rows } });
|
||||||
|
const result = await runner('analyst_query', { layers: ['bikeshare'], scope: { kind: 'anywhere' }, limit: 1 });
|
||||||
|
assert.equal(result.ok, true);
|
||||||
|
assert.equal(result.items[0].name.length, 180);
|
||||||
|
assert.equal(result.items[0].label, 'Town Station');
|
||||||
|
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