Merge branch 'main' into fix/multipart-cable-feature-ownership

This commit is contained in:
Sameh Khamis committed 2026-11-02 07:17:56 -08:00
commit 26f668844e
9 files changed
+110 -10

No files matched your search

@@ -0,0 +1,3 @@
- Bike Share stations whose feed publishes no capacity now take it from each
status poll's bikes and docks instead of keeping the first poll's value or
the placeholder 15 (Aditya, @Rohilalala, #1447).
@@ -0,0 +1,5 @@
- Leave layer aliases the layer id already spells, such as "dams" for
`local-dams`, out of the voice layer hint. The Realtime prompt is 305
characters shorter, leaving room under the 52,000 gate for new
voice-toggleable layers; every alias is still accepted (@quanticsoul4772,
#1462).
+2
View File
@@ -1898,6 +1898,8 @@ limit, as well as when a tile decodes or the source resets.
GBFS null or empty availability counts retain an unknown value through station GBFS null or empty availability counts retain an unknown value through station
parsing and rendering; they use the neutral station color rather than reporting parsing and rendering; they use the neutral station color rather than reporting
an empty dock. Explicit zero and caller-supplied numeric fallbacks remain valid. an empty dock. Explicit zero and caller-supplied numeric fallbacks remain valid.
A station whose `station_information` publishes no capacity takes bikes plus
docks from each status poll (15 until its status arrives), not from the first.
## Terrain height cache bound ## Terrain height cache bound
+3 -2
View File
@@ -1029,10 +1029,11 @@ test('the voice TOOL SCHEMA matches the pinned contract — the mission mapping
// Panel adds timeout and named voice/provider targets; consolidated guidance // Panel adds timeout and named voice/provider targets; consolidated guidance
// preserves the mission mapping and globe-rotation arguments. // preserves the mission mapping and globe-rotation arguments.
// S2 GhostMaps extends the generated layer enums; both feature contracts remain. // S2 GhostMaps extends the generated layer enums; both feature contracts remain.
assert.equal(block.length, 31206, 'serialized tool schema length drifted'); // The layer alias hint leaves out aliases the layer id already spells.
assert.equal(block.length, 30901, 'serialized tool schema length drifted');
assert.equal( assert.equal(
crypto.createHash('sha256').update(block).digest('hex'), crypto.createHash('sha256').update(block).digest('hex'),
'f772df95404494bf492e02fde1beabffdfe61986940bffd0cf9ad5a38d768746', '9e1bd8fde648953c47718ba248758415a95c1484b0da6369df6b89b0505c0de2',
'the first-run missions must ride EXISTING tools: no schema edit, no cache bust', 'the first-run missions must ride EXISTING tools: no schema edit, no cache bust',
); );
const instructions = fs.readFileSync( const instructions = fs.readFileSync(
+59
View File
@@ -446,3 +446,62 @@ test('unknown GBFS counts stay unknown through the source, model and station col
assert.equal(model.toNonNegativeInteger('3'), 3); assert.equal(model.toNonNegativeInteger('3'), 3);
} }
}); });
test('a station without a published capacity re-derives it from every status poll', async () => {
const { createRendering } = await import('./rendering.js');
const { COLOR_YELLOW } = await import('./policy.js');
const model = createModel({});
const state = {
_cityRuntime: new Map(),
_stationRenderMap: new Map(),
_pointCollection: { add: (options) => ({ ...options }) },
};
const rendering = createRendering({
state,
services: { render: { governorRequestRender() {} } },
parts: { model },
});
const info = model.parseStationInformation({
data: {
stations: [
{ station_id: 'open', name: 'Open', lat: 30.26, lon: -97.74 },
{
station_id: 'fixed',
name: 'Fixed',
lat: 30.27,
lon: -97.75,
capacity: 13,
},
],
},
});
rendering.ensureCityPoints('austin', info);
const poll = (stations) =>
rendering.applyStatusToPoints(
'austin',
model.parseStationStatus({ data: { stations } }),
);
const open = state._stationRenderMap.get('austin:open');
const fixed = state._stationRenderMap.get('austin:fixed');
// The first poll has no row for the station: capacity is a placeholder.
poll([
{
station_id: 'fixed',
num_bikes_available: 4,
num_docks_available: 9,
},
]);
poll([
{ station_id: 'open', num_bikes_available: 10, num_docks_available: 20 },
{ station_id: 'fixed', num_bikes_available: 2, num_docks_available: 3 },
]);
assert.equal(open.capacity, 30, 'the first poll fixed the capacity');
assert.equal(open.point.color, COLOR_YELLOW, '10 of 30 is not plenty');
assert.equal(fixed.capacity, 13, 'a published capacity is kept');
poll([
{ station_id: 'open', num_bikes_available: 4, num_docks_available: 2 },
]);
assert.equal(open.capacity, 6);
});
+3 -1
View File
@@ -185,6 +185,7 @@ export function createRendering({
stationName: station.name, stationName: station.name,
point, point,
capacity: parts.model.toNonNegativeInteger(station.capacity), capacity: parts.model.toNonNegativeInteger(station.capacity),
publishedCapacity: parts.model.toNonNegativeInteger(station.capacity),
bikesAvailable: null, bikesAvailable: null,
docksAvailable: null, docksAvailable: null,
isInstalled: station.isInstalled, isInstalled: station.isInstalled,
@@ -245,7 +246,8 @@ export function createRendering({
if (!record) continue; if (!record) continue;
const status = statusMap.get(record.stationId) || null; const status = statusMap.get(record.stationId) || null;
const capacity = resolveCapacity(record.capacity, status); // Resolve from the published capacity, not last poll's derived value.
const capacity = resolveCapacity(record.publishedCapacity, status);
record.capacity = capacity; record.capacity = capacity;
record.bikesAvailable = parts.model.toNonNegativeInteger( record.bikesAvailable = parts.model.toNonNegativeInteger(
+2 -1
View File
@@ -47,7 +47,8 @@ test('the complete Realtime tool payload pins the manifest-generated layer relea
// is consolidated without changing either feature's arguments. // is consolidated without changing either feature's arguments.
// Snow leaves the set_visual_style enum. // Snow leaves the set_visual_style enum.
// The Operator HUD layout leaves set_hud's layout enum. // The Operator HUD layout leaves set_hud's layout enum.
'f47a7f78f13b1f79978fa524fad2618e32bd813e1a5dfd746bf35d29909fc91b', // The layer alias hint leaves out aliases the layer id already spells.
'e65812db60a2702cebb496fca257cb04a28f1bbbf250cb9ab65a3db33005e0c8',
); );
}); });
+15 -6
View File
@@ -525,22 +525,31 @@ const SELF_EVIDENT_IDS = new Set([
'radio', 'radio',
'wind', 'wind',
'directions', 'directions',
// The brand is already the layer id; all spoken aliases remain accepted.
's2-ghostmaps',
]); ]);
/** Lower case with spaces and hyphens removed: "Data centers" → "datacenters". */
const spelled = (text) => text.toLowerCase().replace(/[\s-]+/g, '');
/** /**
* Compact model-facing alias hint: "alias/alias→id; …" for toggleable layers * Compact model-facing alias hint: "alias/alias→id; …" for toggleable layers
* whose id is not already the spoken word. At most two aliases per layer keep * whose id is not already the spoken word. At most two aliases per layer keep
* the tool JSON small; the runner accepts the full alias list. * the tool JSON small; the runner accepts the full alias list. Of those two,
* an alias the id already spells ("dams"→local-dams, "weather radar"→
* weather-radar) is left out, and a layer with neither left gets no hint.
* @returns {string} One line for the layerId description. * @returns {string} One line for the layerId description.
*/ */
export function voiceLayerAliasHint() { export function voiceLayerAliasHint() {
return VOICE_LAYER_MANIFEST.filter( return VOICE_LAYER_MANIFEST.filter(
(entry) => (entry) => entry.toggle && !SELF_EVIDENT_IDS.has(entry.id),
entry.toggle && entry.aliases.length && !SELF_EVIDENT_IDS.has(entry.id),
) )
.map((entry) => `${entry.aliases.slice(0, 2).join('/')}→${entry.id}`) .map((entry) => ({
id: entry.id,
aliases: entry.aliases
.slice(0, 2)
.filter((alias) => !spelled(entry.id).includes(spelled(alias))),
}))
.filter((entry) => entry.aliases.length)
.map((entry) => `${entry.aliases.join('/')}→${entry.id}`)
.join(';'); .join(';');
} }
+18
View File
@@ -9,6 +9,7 @@ import {
VOICE_TOGGLE_LAYER_IDS, VOICE_TOGGLE_LAYER_IDS,
VOICE_CONTEXT_LAYER_IDS, VOICE_CONTEXT_LAYER_IDS,
VOICE_QUERY_LAYER_IDS, VOICE_QUERY_LAYER_IDS,
voiceLayerAliasHint,
voiceLayerAliasMap, voiceLayerAliasMap,
} from './layerManifest.js'; } from './layerManifest.js';
@@ -78,6 +79,23 @@ test('tool enums are generated from the manifest', () => {
assert.ok(panels.includes('weather-panel') && panels.includes('recent-imagery-panel')); assert.ok(panels.includes('weather-panel') && panels.includes('recent-imagery-panel'));
}); });
test('the alias hint leaves out names the layer id already spells', () => {
const hint = voiceLayerAliasHint();
const entries = hint.split(';').map((entry) => entry.split('→'));
const flat = (text) => text.toLowerCase().replace(/[\s-]+/g, '');
for (const [aliases, id] of entries) {
for (const alias of aliases.split('/')) {
assert.ok(!flat(id).includes(flat(alias)), `"${alias}" is already spelled by ${id}`);
}
}
const ids = entries.map(([, id]) => id);
assert.ok(!ids.includes('local-dams'), 'a layer left with no alias gets no entry');
assert.ok(!ids.includes('weather-radar'));
assert.match(hint, /(^|;)aqhi→air-quality(;|$)/, 'an alias the id does not spell stays');
assert.match(hint, /(^|;)hurricanes→weather-cyclones(;|$)/);
assert.match(hint, /(^|;)fires\/wildfires→local-firms(;|$)/);
});
test('spoken aliases resolve to real layers, keep every legacy phrase and never collide', () => { test('spoken aliases resolve to real layers, keep every legacy phrase and never collide', () => {
const aliases = voiceLayerAliasMap(); const aliases = voiceLayerAliasMap();
for (const id of aliases.values()) assert.ok(manifestIds.includes(id), id); for (const id of aliases.values()) assert.ok(manifestIds.includes(id), id);