Merge branch 'main' into fix/node26-allocation-gates

This commit is contained in:
German Khokhlov authored and GitHub committed 2026-11-09 20:19:29 +04:00
commit 015e3ff7fc
738 files changed
+136229 -3138

No files matched your search

+57 -1
View File
@@ -7,13 +7,16 @@
* Natural Earth 10m admin-1 states and provinces (public domain).
* src/data/local_data/us_census_counties/counties.json
* US Census Bureau cartographic boundary counties (public domain).
* src/data/local_data/natural_earth/populated_places.json
* Natural Earth 10m populated places as points, for the orbital view's
* "near <city>" (`src/data/placeBelow.js`; public domain).
*
* Both sources are pinned by URL and SHA-256, so a rerun reproduces the packs
* byte for byte. Downloads are cached outside the repository (default: the
* system temp directory) and are never committed.
*
* Usage:
* node scripts/build-admin-packs.mjs [--cache <dir>] [--only countries|admin1|counties]
* node scripts/build-admin-packs.mjs [--cache <dir>] [--only countries|admin1|counties|places]
*
* The provenance READMEs beside each pack record these parameters.
*/
@@ -101,6 +104,9 @@ export const PARAMS = Object.freeze({
elsewhereMinPopulation: 50_000,
cityStateMaxKm2: 1_000,
},
// Every populated place, kept as a point: name, position and the two
// prominence ranks the orbital view picks "near <city>" by.
places: { decimals: 3 },
});
const args = process.argv.slice(2);
@@ -688,6 +694,47 @@ async function buildAdmin1() {
};
}
async function buildPlaces() {
const params = PARAMS.places;
const source = await fetchPinned('places');
const factor = 10 ** params.decimals;
const round = (value) => Math.round(value * factor) / factor;
const places = JSON.parse(source.bytes.toString('utf8'))
.features.map((f) => f.properties)
.filter(
(p) =>
p.name &&
Number.isFinite(p.latitude) &&
Number.isFinite(p.longitude) &&
Number.isFinite(p.scalerank),
)
.map((p) => [
p.name,
round(p.latitude),
round(p.longitude),
p.scalerank,
Number.isFinite(p.rank_max) ? p.rank_max : 0,
])
.sort((a, b) => a[3] - b[3] || b[4] - a[4] || a[0].localeCompare(b[0]));
return {
meta: {
title: 'Natural Earth populated places',
source: 'ne_10m_populated_places_simple',
url: SOURCES.places.url,
sha256: source.digest,
commit: NE_COMMIT,
license:
'Public domain (https://www.naturalearthdata.com/about/terms-of-use/)',
schema:
'places[]: [name, latitude, longitude, scalerank (0 most prominent .. 10), rank_max (population class, 0..14; higher is larger)]',
decimals: params.decimals,
curation: params,
script: 'scripts/build-admin-packs.mjs',
},
places,
};
}
async function buildCounties() {
const params = PARAMS.counties;
const source = await fetchPinned('counties');
@@ -768,4 +815,13 @@ if (isMain) {
'src/data/local_data/us_census_counties/counties.json',
await buildCounties(),
);
if (!only || only === 'places') {
const relative = 'src/data/local_data/natural_earth/populated_places.json';
const pack = await buildPlaces();
const text = JSON.stringify(pack) + '\n';
await writeFile(path.join(ROOT, relative), text);
console.log(
`${relative}: ${pack.places.length} places, ${Buffer.byteLength(text)} bytes`,
);
}
}
+101
View File
@@ -0,0 +1,101 @@
#!/usr/bin/env node
/**
* Build the owner flag sheet the orbital Cockpit marks a satellite's owner
* country with (`src/ui/ownerFlag.js`): every flag in OWNER_COUNTRY_CODES
* (`src/layers/satellites/classes.js`), drawn from the flag-icons 4:3 SVGs
* at twice the readout's 20 × 15 px, in one PNG the app loads at startup.
*
* src/data/local_data/owner_flags/owner_flags.png the sheet
* src/ui/ownerFlagSheet.js its layout (generated)
*
* Flag i (in the sorted code list) sits at column i % COLUMNS, row
* floor(i / COLUMNS), each cell followed by a GUTTER of transparent pixels. A rerun with the same flag-icons and sharp versions
* reproduces both files.
*
* Usage: node scripts/build-owner-flags.mjs
*/
import { readFile, writeFile } from 'node:fs/promises';
import { createRequire } from 'node:module';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import * as prettier from 'prettier';
import sharp from 'sharp';
import { OWNER_COUNTRY_CODES } from '../src/layers/satellites/classes.js';
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
const require = createRequire(import.meta.url);
const FLAGS_DIR = path.join(
path.dirname(require.resolve('flag-icons/package.json')),
'flags/4x3',
);
const FLAG_ICONS_VERSION = require('flag-icons/package.json').version;
/** One flag's cell, CSS pixels × 2. */
export const CELL = Object.freeze({ width: 40, height: 30 });
/**
* Transparent sheet pixels after each flag, so a browser smoothing a scaled
* flag (a 125% display) never reaches into its neighbour.
*/
export const GUTTER = 2;
export const COLUMNS = 16;
const SHEET = path.join(
ROOT,
'src/data/local_data/owner_flags/owner_flags.png',
);
const LAYOUT = path.join(ROOT, 'src/ui/ownerFlagSheet.js');
async function flagCell(code) {
const svg = await readFile(path.join(FLAGS_DIR, `${code.toLowerCase()}.svg`));
return sharp(svg, { density: 300 })
.resize(CELL.width, CELL.height, { fit: 'fill' })
.png()
.toBuffer();
}
const codes = [...OWNER_COUNTRY_CODES];
const rows = Math.ceil(codes.length / COLUMNS);
const pitchX = CELL.width + GUTTER;
const pitchY = CELL.height + GUTTER;
const cells = await Promise.all(codes.map(flagCell));
const sheet = await sharp({
create: {
width: COLUMNS * pitchX,
height: rows * pitchY,
channels: 4,
background: { r: 0, g: 0, b: 0, alpha: 0 },
},
})
.composite(
cells.map((input, i) => ({
input,
left: (i % COLUMNS) * pitchX,
top: Math.floor(i / COLUMNS) * pitchY,
})),
)
.png({ compressionLevel: 9, palette: true, quality: 100, effort: 10 })
.toBuffer();
await writeFile(SHEET, sheet);
const layout = `// Generated by scripts/build-owner-flags.mjs from flag-icons ${FLAG_ICONS_VERSION}; do not edit.
/** The owner flag sheet's layout: flag i at column i % columns, row i / columns. */
export const OWNER_FLAG_SHEET = Object.freeze({
cellWidthPx: ${CELL.width},
cellHeightPx: ${CELL.height},
gutterPx: ${GUTTER},
columns: ${COLUMNS},
rows: ${rows},
codes: Object.freeze(${JSON.stringify(codes)}),
});
`;
await writeFile(
LAYOUT,
await prettier.format(layout, {
...(await prettier.resolveConfig(LAYOUT)),
filepath: LAYOUT,
}),
);
console.log(
`Wrote ${codes.length} flags (${COLUMNS} × ${rows}), ${(sheet.length / 1024).toFixed(1)} KB.`,
);
+118
View File
@@ -0,0 +1,118 @@
#!/usr/bin/env python3
'''Maintainer-only UCS Satellite Database pack build (openpyxl).
Reads the UCS Excel release and writes the compact pack the satellites layer
loads: one row per satellite, keyed by NORAD number, with string-table indexes
for the repeated text columns. Starlink rows are left out: the layer files
every STARLINK object under its own class and operator without them.
Usage: python scripts/build-ucs-satellites.py "<path>/UCS-Satellite-Database 5-1-2023.xlsx"
'''
import argparse
import json
import logging
from pathlib import Path
import re
import openpyxl
LOG = logging.getLogger('build-ucs-satellites')
ROOT = Path(__file__).resolve().parent.parent
PACK = ROOT / 'src/data/local_data/ucs_satellites'
VERSION = 'UCS-Satellite-Database 5-1-2023'
DATA_CURRENT_TO = '2023-05-01'
URL = 'https://www.ucsusa.org/satellite_database'
# Output column -> spreadsheet header.
COLUMNS = {
'operator': 'Operator/Owner',
'operatorCountry': 'Country of Operator/Owner',
'purpose': 'Purpose',
}
STARLINK = re.compile(r'\bstarlink\b', re.I)
def clean(value: object) -> str:
'''One cell as tidy text; UCS's placeholders for nothing become ''.'''
text = re.sub(r'\s+', ' ', str(value or '')).strip()
return '' if text.lower() in ('', 'none', 'n/a') else text
def build(xlsx: Path) -> dict:
'''Read the release's first sheet into the pack the layer loads.'''
sheet = openpyxl.load_workbook(xlsx, read_only=True, data_only=True).worksheets[0]
rows = sheet.iter_rows(values_only=True)
header = {str(h).strip(): i for i, h in enumerate(next(rows)) if h}
names = [header['Current Official Name of Satellite'], header['Name of Satellite, Alternate Names']]
norad_col = header['NORAD Number']
tables = {key: [''] for key in COLUMNS}
index = {key: {'': 0} for key in COLUMNS}
records = {}
source_rows = skipped_starlink = invalid_norad = 0
for row in rows:
if not any(row):
continue
source_rows += 1
if any(STARLINK.search(clean(row[i])) for i in names):
skipped_starlink += 1
continue
try:
norad = int(float(row[norad_col]))
except (TypeError, ValueError):
invalid_norad += 1
continue
if norad <= 0:
invalid_norad += 1
continue
record = [norad]
for key, column in COLUMNS.items():
text = clean(row[header[column]])
if text not in index[key]:
index[key][text] = len(tables[key])
tables[key].append(text)
record.append(index[key][text])
# A duplicate NORAD number keeps the first row, as UCS lists it.
records.setdefault(norad, record)
ordered = [records[n] for n in sorted(records)]
return {
'meta': {
'source': 'Union of Concerned Scientists Satellite Database',
'version': VERSION,
'url': URL,
'dataCurrentTo': DATA_CURRENT_TO,
'license': 'Free, unrestricted use; acknowledgement requested (UCS database manual, section 3)',
'curation': {
'sourceRows': source_rows,
'starlinkRowsDropped': skipped_starlink,
'invalidNoradDropped': invalid_norad,
'duplicateNoradDropped': source_rows - skipped_starlink - invalid_norad - len(ordered),
'columns': COLUMNS,
},
'recordCount': len(ordered),
},
'fields': ['noradId', *COLUMNS],
'strings': tables,
'records': ordered,
}
def main() -> None:
'''Build the pack from the release given on the command line.'''
logging.basicConfig(level=logging.INFO, format='%(message)s')
parser = argparse.ArgumentParser(description='Build the UCS satellite pack from the UCS Excel release.')
parser.add_argument('xlsx', type=Path, help='UCS-Satellite-Database <version>.xlsx')
pack = build(parser.parse_args().xlsx)
PACK.mkdir(parents=True, exist_ok=True)
out = PACK / 'ucs_satellites.json'
# LF on every platform: the repository stores text with LF endings.
out.write_text(
json.dumps(pack, ensure_ascii=False, separators=(',', ':')) + '\n',
encoding='utf-8',
newline='\n',
)
LOG.info('%s: %d records, %d bytes', out.relative_to(ROOT), pack['meta']['recordCount'], out.stat().st_size)
if __name__ == '__main__':
main()
+1 -1
View File
@@ -44,7 +44,7 @@ CCTV_NSW_ENABLED="${CCTV_NSW_ENABLED:-1}"
CCTV_NSW_MAX_SOURCES="${CCTV_NSW_MAX_SOURCES:-250}"
CCTV_CALGARY_ENABLED="${CCTV_CALGARY_ENABLED:-1}"
CCTV_CALGARY_MAX_SOURCES="${CCTV_CALGARY_MAX_SOURCES:-220}"
CCTV_MAX_SOURCES="${CCTV_MAX_SOURCES:-4000}"
CCTV_MAX_SOURCES="${CCTV_MAX_SOURCES:-7350}"
# Capture which provider credentials genuinely came from the parent shell
# before this launcher resolves dotenv and Keychain fallbacks. Only names are
+261 -1
View File
@@ -57,6 +57,9 @@
"src/app/mapLifecycle.test.mjs",
"src/app/startupCamera.test.mjs",
"src/app/viewer.js",
"src/app/zoomOutGuard.test.mjs",
"src/app/controls.test.mjs",
"scripts/qa-zoom-out-guard.mjs",
"src/app/atmosphereCompat.test.mjs",
"src/standalone/application.js",
"src/standalone/controls.js",
@@ -109,6 +112,8 @@
"src/data/terrainHeightsProxy.test.mjs",
"src/data/tomtomTiles.js",
"src/data/tomtomTiles.test.mjs",
"src/data/tileBudget.js",
"src/data/tileBudget.test.mjs",
"src/data/firmsCsv.js",
"src/data/firmsCsv.test.mjs",
"src/tooling/environmentProviders.test.mjs",
@@ -325,6 +330,12 @@
"src/ui/styles/responsive.css",
"src/ui/styles/voice-cost.css",
"src/ui/styles/cockpit.css",
"src/ui/styles/cockpitBridge.css",
"src/ui/styles/cockpitRadar.css",
"src/ui/styles/cockpitOrbital.css",
"src/layers/satellites/sensors.js",
"src/layers/satellites/sensors.test.mjs",
"src/data/imagingSensors.js",
"src/ui/styles/command-dock-compact.css",
"src/app/stateChannel.js",
"src/app/stateChannel.test.mjs",
@@ -344,6 +355,7 @@
"src/layers/flights/tracking.js",
"src/layers/flights/rendering.js",
"src/layers/flights/testing.js",
"src/layers/flights/hiddenContactRotation.test.mjs",
"src/layers/flights/evidence.js",
"src/layers/flights/ingestion.js",
"src/layers/flights/queries.js",
@@ -402,12 +414,22 @@
"src/layers/perimeters/cards.test.mjs",
"src/layers/perimeters/inciweb.test.mjs",
"src/layers/perimeters/ownership.test.mjs",
"src/layers/droneAirspace/index.js",
"src/layers/droneAirspace/records.js",
"src/layers/droneAirspace/source.js",
"src/layers/droneAirspace/cards.js",
"src/layers/droneAirspace/records.test.mjs",
"src/layers/droneAirspace/source.test.mjs",
"src/layers/droneAirspace/cards.test.mjs",
"src/layers/droneAirspace/ownership.test.mjs",
"src/layers/droneAirspace/docs.test.mjs",
"src/data/firmsHeatmap.js",
"src/data/fireAnchors.js",
"src/layers/firms/anchors.js",
"src/layers/firms/cards.js",
"src/layers/firms/index.js",
"src/layers/firms/ingestion.js",
"src/layers/firms/heatVisibility.test.mjs",
"src/layers/firms/lifecycle.js",
"src/layers/firms/model.js",
"src/layers/firms/policy.js",
@@ -461,6 +483,7 @@
"src/layers/satellites/stale.test.mjs",
"src/layers/launches/source.test.mjs",
"src/layers/launches/ingestion.test.mjs",
"src/layers/launches/failedEntry.test.mjs",
"src/layers/installations/lifecycle.js",
"src/layers/installations/viewport.js",
"src/layers/installations/controls.js",
@@ -568,7 +591,9 @@
"src/data/cctvHk.test.mjs",
"src/data/cctvVegvesen.test.mjs",
"src/data/cctvA22.test.mjs",
"src/data/cctvIceland.test.mjs",
"src/data/cctvNyc.test.mjs",
"src/data/cctvHouston.test.mjs",
"src/data/cctvNswSource.test.mjs",
"src/data/cctvTxdotSource.test.mjs",
"src/layers/cctv/source.test.mjs",
@@ -681,6 +706,7 @@
"src/app/layers/radio.js",
"src/app/layers/earthquakes.js",
"src/app/layers/perimeters.js",
"src/app/layers/droneAirspace.js",
"src/app/layers/cctv.js",
"src/app/layers/firms.js",
"src/app/layers/satellites.js",
@@ -771,6 +797,7 @@
"server/providers/transit.js",
"server/providers/transitHistory.js",
"server/providers/transitHistory.test.mjs",
"src/data/rioRouteNames.js",
"src/data/transitFeeds.js",
"src/data/transitFeeds.test.mjs",
"src/data/transitProxy.js",
@@ -866,6 +893,7 @@
"src/layers/weather/imageryOrder.test.mjs",
"server/providers/cyclones.js",
"server/providers/firePerimeters.js",
"server/providers/droneAirspace.js",
"src/data/cyclonesProxy.test.mjs",
"src/data/firePerimetersProxy.test.mjs",
"server/providers/gnssIntegrity.js",
@@ -890,6 +918,7 @@
"src/layers/ghostMaps/records.test.mjs",
"src/layers/ghostMaps/lifecycle.test.mjs",
"scripts/qa-ghostmaps.mjs",
"src/data/droneAirspaceProxy.test.mjs",
"src/layers/cyclones/index.js",
"src/layers/cyclones/rendering.js",
"src/layers/cyclones/labels.js",
@@ -1099,6 +1128,7 @@
"src/sky/starrySky.test.mjs",
"src/tooling/skyAssets.test.mjs",
"src/tooling/skyAssets.paintOut.test.mjs",
"src/tooling/responseCompression.test.mjs",
"scripts/qa-anime-sky.mjs",
"scripts/qa-sky-model-lighting.mjs",
"server/providers/overpass/requestAdmission.test.mjs",
@@ -1109,7 +1139,237 @@
"scripts/qa-status-badges.mjs",
"src/dockerTrustedPeers.test.mjs",
"src/cameraGlobeRotation.test.mjs",
"src/cameraTilt.test.mjs",
"src/ui/shellFacade.test.mjs",
"src/voice/globeResetConfirmation.test.mjs",
"src/hudDms.test.mjs",
"server/providers/common/rateLimitKeys.test.mjs"
"src/hudSummaryGate.test.mjs",
"src/hudSummaryLine.test.mjs",
"src/testSupport/stubMgrs.mjs",
"src/tools/queries/hud-caption-feed-state.test.mjs",
"src/voice/hudPlaceLabels.test.mjs",
"src/voice/panel/preferences.test.mjs",
"src/voice/panel/preferencesView.test.mjs",
"src/voice/voiceControlView.test.mjs",
"src/voice/panel/host.test.mjs",
"src/voice/panel/manualPanel.test.mjs",
"src/voice/panel/nativeActivity.test.mjs",
"src/voice/panel/inactivity.test.mjs",
"src/voice/panel/actionObserver.test.mjs",
"src/voice/realtimeCostPanel.test.mjs",
"docs/SHARE-LINK-FORMAT.md",
"src/share/wire.test.mjs",
"src/share/viewCodec.test.mjs",
"src/share/legacyShareState.test.mjs",
"src/testSupport/shareViewFixtures.mjs",
"src/ui/shareLinks.test.mjs",
"src/ui/shareButton.test.mjs",
"src/share/shareLinkPath.test.mjs",
"src/ui/kit/UiRail.test.mjs",
"src/ui/kit/UiSegmented.vue",
"src/ui/kit/UiSegmented.test.mjs",
"src/ui/kit/UiSlider.js",
"src/ui/kit/UiSlider.test.mjs",
"src/ui/kit/UiButton.vue",
"src/ui/kit/UiButton.test.mjs",
"src/ui/kit/UiChip.vue",
"src/ui/kit/UiChip.test.mjs",
"src/ui/globeActions.test.mjs",
"src/ui/safeFrameView.test.mjs",
"src/ui/kit/UiSelect.js",
"src/ui/kit/UiSelect.test.mjs",
"src/ui/kit/UiBadge.vue",
"src/ui/kit/UiBadge.test.mjs",
"src/ui/kit/UiEmpty.vue",
"src/ui/rail/RailChips.vue",
"src/ui/displayPanelActions.test.mjs",
"src/testSupport/vueDom.mjs",
"build/vueLoader.js",
"build/registerVue.js",
"src/ui/panelNavigation.test.mjs",
"src/ui/stores/owners.js",
"src/ui/stores/owners.test.mjs",
"src/ui/stores/panels.test.mjs",
"src/ui/stores/status.js",
"src/ui/statusChipsView.test.mjs",
"src/ui/statusChipOwners.test.mjs",
"src/ui/toast.js",
"src/ui/toastView.test.mjs",
"src/ui/stores/sceneInteractions.js",
"src/scenes/dataPacks/presentationView.test.mjs",
"src/ui/stores/dataPacks.js",
"src/scenes/interactionsView.test.mjs",
"src/layers/wind/inspectionMarker.js",
"src/layers/wind/inspectionMarker.test.mjs",
"src/ui/stores/windInspection.js",
"src/ui/stores/missionReplay.js",
"src/layers/launches/replayOverlay.test.mjs",
"src/ui/stores/bhoteKoshiPanel.js",
"src/data/bhoteKoshiPanelView.test.mjs",
"src/ui/stores/celestialRing.js",
"src/celestialRingView.test.mjs",
"src/ui/stores/screenAnnotations.js",
"src/ui/stores/worldOverlayActions.js",
"src/overlays/worldOverlayActionsView.test.mjs",
"src/ui/stores/awarenessDirection.js",
"src/layers/awareness/directionOverlay.test.mjs",
"src/awarenessEcho.js",
"src/awarenessEcho.test.mjs",
"src/data/detectionAwarenessEcho.test.mjs",
"src/hudReadouts.js",
"src/ui/stores/hud.js",
"src/ui/views/intelHudView.test.mjs",
"src/testSupport/hudReadouts.mjs",
"src/ui/stores/firstRun.js",
"src/firstRunLauncherView.test.mjs",
"src/firstRunCopy.js",
"src/data/headingDegrees.test.mjs",
"src/data/distanceFormat.test.mjs",
"src/sources/adsbFields.test.mjs",
"src/data/firmsIntensity.js",
"src/data/firmsIntensity.test.mjs",
"src/ui/layerPanel/RowSwitch.vue",
"src/ui/layerPanel/RowSlider.vue",
"src/sources/phaseTiming.test.mjs",
"scripts/build-census-places.mjs",
"src/data/placeBoundaries.test.mjs",
"src/data/boundaryLabels.test.mjs",
"src/data/packGeometry.test.mjs",
"src/annotations/placeOutline.test.mjs",
"src/annotations/outlineIdentity.test.mjs",
"src/annotations/outlineRungs.test.mjs",
"src/annotations/outlineRungsFallback.test.mjs",
"src/search/photonSetting.test.mjs",
"src/sources/nominatimOutlines.test.mjs",
"src/sources/openFreeMapOutlines.test.mjs",
"src/sources/openFreeMapOutlinesEdges.test.mjs",
"src/standalone/layerSources.test.mjs",
"src/tooling/nominatimGuardLifecycle.test.mjs",
"src/tooling/nominatimLockOwnership.test.mjs",
"src/tooling/nominatimOutlineRoute.test.mjs",
"server/providers/common/rateLimitKeys.test.mjs",
"src/voice/areaCandidateContract.js",
"src/voice/layerActionOwnership.js",
"src/data/areaGeometry.test.mjs",
"src/data/areaResolver.test.mjs",
"src/sources/osmFeatures.test.mjs",
"src/layers/osmPlaces/index.test.mjs",
"src/tooling/boundedCache.test.mjs",
"src/tooling/osmFeaturesRoute.test.mjs",
"src/voice/areaActions.test.mjs",
"src/voice/imageryActions.test.mjs",
"src/voice/osmActions.test.mjs",
"src/voice/realtimeProtocol.test.mjs",
"src/layers/awareness/rangeRing.test.mjs",
"src/voice/voiceMiniPreview.js",
"src/voice/voiceMiniPreview.test.mjs",
"src/voice/chatPreferences.test.mjs",
"src/voice/textChat.test.mjs",
"src/voice/chatAttachments.test.mjs",
"src/voice/chatControlView.test.mjs",
"src/textChatServer.test.mjs",
"src/geminiChatServer.test.mjs",
"src/voice/geminiTextChat.test.mjs",
"server/providers/common/textChat.js",
"server/providers/common/textInstructions.js",
"server/providers/common/textTools.js",
"server/providers/gemini/chat.js",
"server/providers/openai/chat.js",
"src/ui/styles/ai-chat.css",
"src/voice/chatAttachments.js",
"src/voice/chatControl.js",
"src/voice/chatPreferences.js",
"src/voice/geminiTextChat.js",
"src/voice/panelLayout.js",
"src/voice/textChat.js",
"src/voice/textProtocol.js",
"src/app/sourceComposition.js",
"src/app/sourceComposition.test.mjs",
"src/data/dataCredits.js",
"src/data/layerState.js",
"src/data/layerState.test.mjs",
"src/data/spriteOrder.js",
"src/i18n/locales/ar.json",
"src/i18n/locales/de.json",
"src/i18n/locales/es.json",
"src/i18n/locales/fr.json",
"src/i18n/locales/it.json",
"src/i18n/locales/ja.json",
"src/i18n/locales/pt.json",
"src/i18n/locales/ru.json",
"src/i18n/locales/uk.json",
"src/i18n/locales/zh-CN.json",
"src/i18n/locales/zh-Hant.json",
"src/i18n/same-as-english.json",
"src/navigationPolicy.js",
"src/overlays/worldOverlayDraw.js",
"src/overlays/worldOverlayDraw.test.mjs",
"src/sources/rateLimit.js",
"src/ui/layerNames.js",
"src/ui/styles/cyber.css",
"src/voice/layerManifest.js",
"src/worldFocus.js",
"src/worldFocus.test.mjs",
"server/providers/acartia.js",
"server/providers/acartia.test.mjs",
"server/providers/common/userAgent.js",
"server/providers/inaturalist/client.js",
"server/providers/inaturalist/observations.js",
"server/providers/inaturalist/observations.test.mjs",
"src/app/layers/wildlifeSightings.js",
"src/layers/wildlifeSightings/cards.js",
"src/layers/wildlifeSightings/cards.test.mjs",
"src/layers/wildlifeSightings/controls.js",
"src/layers/wildlifeSightings/docs.test.mjs",
"src/layers/wildlifeSightings/index.js",
"src/layers/wildlifeSightings/lifecycle.js",
"src/layers/wildlifeSightings/model.js",
"src/layers/wildlifeSightings/model.test.mjs",
"src/layers/wildlifeSightings/ownership.test.mjs",
"src/layers/wildlifeSightings/photos.js",
"src/layers/wildlifeSightings/photos.test.mjs",
"src/layers/wildlifeSightings/policy.js",
"src/layers/wildlifeSightings/records.js",
"src/layers/wildlifeSightings/records.test.mjs",
"src/layers/wildlifeSightings/rendering.js",
"src/layers/wildlifeSightings/selection.js",
"src/layers/wildlifeSightings/source.js",
"src/layers/wildlifeSightings/source.test.mjs",
"src/layers/wildlifeSightings/viewport.js",
"src/layers/wildlifeSightings/viewport.test.mjs",
"src/sources/rateLimit.test.mjs",
"src/layers/wildlifeSightings/bounds.js",
"src/layers/wildlifeSightings/acartiaTaxa.js",
"src/layers/wildlifeSightings/acquisition.test.mjs",
"server/providers/inaturalist/client.test.mjs",
"src/layers/wildlifeSightings/acartiaTaxa.test.mjs",
"src/ui/wildlifeSightingCard.test.mjs",
"src/overlays/wildlifeSightingOverlay.test.mjs",
"scripts/run-perf-tests.mjs",
"src/perfTestRunner.test.mjs",
"src/testSupport/wildlifeBillboardModel.mjs",
"src/layers/wildlifeSightings/gridViewport.test.mjs",
"src/layers/wildlifeSightings/rendering.test.mjs",
"src/layers/wildlifeSightings/sprites.test.mjs",
"src/layers/wildlifeSightings/performance.test.mjs",
"src/layers/wildlifeSightings/model.perf.test.mjs",
"src/ui/wildlifeSightingPlacement.js",
"server/providers/inaturalist/queue.js",
"server/providers/inaturalist/queue.test.mjs",
"server/providers/inaturalist/grid.js",
"server/providers/inaturalist/grid.test.mjs",
"server/providers/inaturalist/grid.fixture.mjs",
"src/layers/wildlifeSightings/performance.js",
"src/layers/wildlifeSightings/renderPerformanceFixtures.mjs",
"server/providers/inaturalist/speed.test.mjs",
"src/layers/wildlifeSightings/render.perf.test.mjs",
"src/layers/wildlifeSightings/cache.integration.test.mjs",
"src/layers/wildlifeSightings/density.test.mjs",
"src/layers/wildlifeSightings/selection.test.mjs",
"server/providers/inaturalist/proxy.fixture.mjs",
"scripts/build-owner-flags.mjs",
"src/ui/ownerFlagSheet.js",
"src/data/placePacksLoading.test.mjs",
"src/ui/placePacksLoading.js",
"src/ui/placePacksLoading.test.mjs"
]
File diff suppressed because it is too large. Load diff
+2
View File
@@ -19,6 +19,8 @@ export const PINOKIO_CONFIG_FIELDS = Object.freeze([
'LL2_API_TOKEN',
'GEV_RATELIMIT_OPENAI_PER_MIN',
'GEV_RATELIMIT_GOOGLE_PER_MIN',
'GEV_RATELIMIT_OPENAI_CHAT_PER_MIN',
'GEV_RATELIMIT_GEMINI_CHAT_PER_MIN',
'PINOKIO_SHARE_CLOUDFLARE',
'PINOKIO_SHARE_LOCAL',
'PINOKIO_SHARE_VAR',
+73
View File
@@ -28,6 +28,8 @@
* (vii) FAR SIDE — pins skip the depth test, so they are horizon-culled:
* seen from over the Indian Ocean no pin clearly behind the planet is
* shown; back over North America the near-side pins show again.
* (viii) AGE — seven days is the default; shorter/longer windows and All
* markers update the globe, and the age choice survives a reload.
*
* Screenshots go to qa-shots/ghostmaps-*.png (gitignored).
*
@@ -224,6 +226,77 @@ async function main() {
record('LIVE: the /api/ghostmaps/events stream is open', stream);
await page.screenshot({ path: path.join(SHOTS_DIR, 'ghostmaps-live.png') });
const ageChips = await page.evaluate(
(id) =>
[
...document.querySelectorAll(
`[data-layer-id="${id}"] [data-segmented-id="age"] [role="radio"]`,
),
].map((option) => ({
id: option.dataset.chipId,
active: option.getAttribute('aria-checked') === 'true',
})),
LAYER_ID,
);
record(
'AGE: seven days is the default with an All markers option',
live.ageDays === 7 &&
ageChips.length === 6 &&
ageChips
.filter((chip) => chip.active)
.map((chip) => chip.id)
.join() === 'age-7' &&
ageChips.some((chip) => chip.id === 'age-all'),
);
const selectAge = async (days) => {
await showRow(page);
const selector = `[data-layer-id="${LAYER_ID}"] [data-chip-id="age-${days ?? 'all'}"]`;
await page.waitForSelector(selector, { timeout: 15000 });
// Dispatch through the rendered button, matching the product-chip gate.
await page.$eval(selector, (button) => button.click());
await page.waitForFunction(
(id, age) => {
const s = window.__godsEyeView.dataManager.layers
.get(id)
.module.getStats();
return s.ageDays === age && !s.loading;
},
{ timeout: TIMEOUT_MS },
LAYER_ID,
days,
);
return stats(page);
};
const all = await selectAge(null);
record(
'AGE: All markers restores old events without duplicate data sources',
all.count > live.count &&
(await sources(page)).length === live.products.length,
`${live.count} → ${all.count}`,
);
await page.reload({ waitUntil: 'domcontentloaded' });
await page.waitForFunction(() => window.__godsEyeView?.dataManager, {
timeout: TIMEOUT_MS,
});
await enableAndSettle(page);
const persistedAge = await stats(page);
record(
'AGE: All markers survives a reload',
persistedAge.ageDays === null && persistedAge.count === all.count,
);
const thirty = await selectAge(30);
const one = await selectAge(1);
const seven = await selectAge(7);
record(
'AGE: counts follow the window order and seven days restores the default',
one.count <= live.count &&
thirty.count >= live.count &&
thirty.count <= all.count &&
seven.count === live.count &&
seven.ageDays === 7,
`1d=${one.count}, 7d=${seven.count}, 30d=${thirty.count}, all=${all.count}`,
);
// ---- (ii) PICK ----------------------------------------------------------
const target = await page.evaluate((id) => {
const gev = window.__godsEyeView;
+33 -1
View File
@@ -296,6 +296,22 @@ async function expandPanel(page) {
);
}
/** A keyless bundle opens on the first keyless source with live controls. */
async function assertKeylessDefault(page) {
await page.evaluate(() =>
window.__godsEyeView.dataManager.setEnabled('street-level', true, {
origin: 'user',
}),
);
await uiUntil(page, litAlone, 'kartaview');
const shown = await page.evaluate(() => ({
status: document.getElementById('sl-status').textContent,
disabled: document.getElementById('sl-controls').disabled,
}));
assert.notEqual(shown.status, 'KEY REQUIRED');
assert.equal(shown.disabled, false, 'controls stay live');
}
async function assertKeyRequired(page) {
await page.evaluate(() =>
window.__godsEyeView.dataManager.setEnabled('street-level', true, {
@@ -528,11 +544,25 @@ async function main() {
);
await expandPanel(page);
if (!configured) {
if (!configured && litAlone(await getUI(page), 'mapillary')) {
// A keyed bundle behind a server without the token keeps Mapillary.
await step('keyless server: KEY REQUIRED gates the controls', () =>
assertKeyRequired(page),
);
skip('no Mapillary key', 'the keyed steps');
} else if (!configured) {
await step('keyless server: opens on KartaView, not KEY REQUIRED', () =>
assertKeylessDefault(page),
);
await step(
'keyless server: the MAPILLARY chip explains its key instead of switching',
async () => {
await press(page, '#sl-provider-chips [data-chip-id="mapillary"]');
await new Promise((resolve) => setTimeout(resolve, 500));
assert.ok(litAlone(await getUI(page), 'kartaview'));
},
);
skip('no Mapillary key', 'the keyed steps');
} else {
await park(page);
await step(
@@ -751,6 +781,8 @@ async function main() {
await park(page);
await fixtureStep(
// The fixture bundle carries a token, so this covers a keyless
// server behind a keyed bundle; the keyless default is checked above.
'keyless server (second page): KEY REQUIRED gates the controls',
async () => {
const keyless = await open(
+235 -14
View File
@@ -29,6 +29,7 @@
* node scripts/qa-voice-routing.mjs --layer behavior --deixis-only --headful --shots qa-shots/point-and-ask
* node scripts/qa-voice-routing.mjs --layer deixis --budget 12 # pointer_context routing (model turns)
* node scripts/qa-voice-routing.mjs --layer deixis-live --url ... # in-app text turns (model turns)
* node scripts/qa-voice-routing.mjs --layer behavior --behavior geometry # areas, drawn counts, imagery
*
* House gotchas honored: camera.cancelFlight() before every teleport; puppeteer
* suites must run sequentially with other harnesses (SwiftShader saturation);
@@ -75,7 +76,7 @@ const LAYER = getOpt('--layer', 'all'); // routing | behavior | all | deixis | d
const TURN_BUDGET = Number(getOpt('--budget', '120'));
const PHRASES_PER_SESSION = Number(getOpt('--batch', '6'));
const ONLY = getOpt('--only', null); // substring filter on phrase text
const BEHAVIOR = getOpt('--behavior', 'all'); // all | outlines | acceptance
const BEHAVIOR = getOpt('--behavior', 'all'); // all | outlines | acceptance | geometry
const DEIXIS_ONLY = process.argv.includes('--deixis-only'); // behavior: point-and-ask block only
const HEADFUL = process.argv.includes('--headful'); // real GPU, visible window
const SHOT_DIR = getOpt('--shots', null); // point-and-ask evidence screenshots
@@ -164,7 +165,11 @@ async function mintSession() {
throw new Error(
`token mint returned no client secret: ${JSON.stringify(body).slice(0, 160)}`,
);
return { value, model };
// The tools this server offered (osm_query only with an operator Overpass).
const tools = new Set(
(body?.session?.tools || []).map((tool) => tool?.name).filter(Boolean),
);
return { value, model, tools };
}
/** One Realtime WS session that can run several text turns sequentially. */
@@ -371,8 +376,10 @@ async function runRoutingLayer() {
}
const batch = list.slice(i, i + PHRASES_PER_SESSION);
let session;
let offered = new Set();
try {
const { value, model } = await mintSession();
const { value, model, tools } = await mintSession();
offered = tools;
session = new RoutingSession(value, model, evidence);
await session.connect();
} catch (e) {
@@ -390,6 +397,14 @@ async function runRoutingLayer() {
skipped(`route: "${p.phrase}"`, 'turn budget exhausted');
continue;
}
const missing = (p.requires || []).filter((name) => !offered.has(name));
if (missing.length) {
skipped(
`route: "${p.phrase}"`,
`${missing.join(', ')} not offered (no configured Overpass)`,
);
continue;
}
turnsUsed += 1;
let calls = [];
try {
@@ -1361,7 +1376,11 @@ async function runBehaviorLayer() {
// The point-and-ask block skips the first-run launcher so its evidence
// screenshots show the map; the full run keeps the historical URL.
const behaviorUrl = new URL(APP_URL);
if (DEIXIS_ONLY || BEHAVIOR === 'acceptance')
if (
DEIXIS_ONLY ||
BEHAVIOR === 'acceptance' ||
BEHAVIOR === 'geometry'
)
behaviorUrl.searchParams.set('welcome', '0');
await page.goto(behaviorUrl.href, {
waitUntil: 'domcontentloaded',
@@ -1420,6 +1439,10 @@ async function runBehaviorLayer() {
await runOutlineAcceptance({ page, run, settle, overpassRequests });
return;
}
if (BEHAVIOR === 'geometry') {
await runGeometryScenarios({ page, run, settle, overpassRequests });
return;
}
// (1) fly_to_location lands near the target. Annotations follow IMMEDIATELY
// while the camera is local — the resolver's proximity gate rejects far
@@ -2298,9 +2321,11 @@ async function runBehaviorLayer() {
`result=${JSON.stringify(r)?.slice(0, 120)} state=${JSON.stringify(contextExited)?.slice(0, 80)}`,
);
// (9f) With Contacts OFF the entry gate is shut. Entry must refuse with the
// reason rather than produce the half-entered state the operator saw: a
// plane anchored under the camera with no HUD and no exit control.
// (9f) Cockpit needs only the tracked aircraft's own layer, not Contacts.
// With Contacts OFF, status must explain a shut gate without blaming
// Contacts, and entry must either enter cleanly or refuse with the reason
// rather than produce the half-entered state the operator saw: a plane
// anchored under the camera with no HUD and no exit control.
r = await run('control_cockpit', { action: 'status' });
const gateReport = r?.state || {};
const cockpitBeforeGate = await page.evaluate(() =>
@@ -2308,10 +2333,13 @@ async function runBehaviorLayer() {
);
report(
gateReport.active === false &&
gateReport.entryAllowed === false &&
gateReport.entryBlockedReason === 'contacts-inactive' &&
(gateReport.entryAllowed === true
? gateReport.entryBlockedReason === null
: ['subject-layer-off', 'no-tracked-aircraft'].includes(
gateReport.entryBlockedReason,
)) &&
!cockpitBeforeGate,
'behavior: cockpit status names why entry is blocked with Contacts off',
'behavior: cockpit status explains its gate without requiring Contacts',
`state=${JSON.stringify(gateReport)?.slice(0, 150)}`,
);
@@ -2325,12 +2353,16 @@ async function runBehaviorLayer() {
Boolean(window.__godsEyeView?.styleManager?.cockpitView?.active),
);
report(
gated?.ok === false &&
/contacts/i.test(gated?.error || '') &&
!cockpitAfterGate,
'behavior: cockpit entry with Contacts off is refused, not half-entered',
gated?.ok === true
? cockpitAfterGate
: !cockpitAfterGate && !/contacts/i.test(gated?.error || ''),
'behavior: cockpit entry with Contacts off follows the own-layer gate, never half-entered',
`ok=${gated?.ok} error=${String(gated?.error || '').slice(0, 90)} active=${cockpitAfterGate}`,
);
if (cockpitAfterGate)
await page.evaluate(() =>
window.__godsEyeView?.styleManager?.controlCockpit?.('exit'),
);
await runBundledOutlineScenarios({
page,
@@ -2340,6 +2372,8 @@ async function runBehaviorLayer() {
resolverLog,
packTimes,
});
// Voice geometry last: it moves the camera and turns layers off.
await runGeometryScenarios({ page, run, settle, overpassRequests });
const shotDir = path.join(ROOT, 'qa-shots');
fs.mkdirSync(shotDir, { recursive: true });
@@ -2483,6 +2517,193 @@ const OUTLINE_ASKS = [
{ entityKind: 'building', latitude: 30.27472, longitude: -97.74035 }, 'building'],
].map(([group, target, lat, lon, height, extra = {}, shot = null]) => ({ group, target, lat, lon, height, extra, shot }));
/**
* Voice geometry, runner-driven: reusable areas, drawn-area counts, a
* landmark area and recent imagery, all without a default Overpass request.
* Public Nominatim use is read from the server's daily count.
*/
async function runGeometryScenarios({ page, run, settle, overpassRequests }) {
const shotDir = path.join(ROOT, 'qa-shots', 'pr-geometry');
fs.mkdirSync(shotDir, { recursive: true });
const shot = (name) => page.screenshot({ path: path.join(shotDir, `${name}.png`) }).catch(() => {});
const nominatimStart = nominatimCount();
const overpassStart = overpassRequests.length;
const teleport = (lat, lon, height, pitch = -90) => page.evaluate(({ lat, lon, height, pitch }) => {
const { viewer } = window.__godsEyeView;
viewer.camera.cancelFlight();
viewer.camera.setView({
destination: viewer.scene.globe.ellipsoid.cartographicToCartesian({
longitude: (lon * Math.PI) / 180, latitude: (lat * Math.PI) / 180, height,
}),
orientation: { heading: 0, pitch: (pitch * Math.PI) / 180, roll: 0 },
});
}, { lat, lon, height, pitch });
const timings = {};
// (G1) "Outline Bagmati, then count fires inside it".
await teleport(27.7, 85.4, 400_000);
await settle(1500);
let t0 = Date.now();
const bagmati = await run('resolve_area', { query: 'Bagmati province', draw: true, flyTo: false });
timings.bagmatiMs = Date.now() - t0;
const bagmatiRefused = [
'AREA_IDENTITY_MISMATCH',
'AREA_IDENTITY_UNVERIFIED',
].includes(bagmati?.code);
const bagmatiVerified =
bagmati?.ok === true &&
bagmati?.drawn === true &&
bagmati?.level === 'admin1' &&
bagmati?.name === 'Bagmati Province' &&
bagmati?.sourceIdentity === 'Bagmati Province';
report(
bagmatiVerified || bagmatiRefused,
'geometry: "outline Bagmati province" uses verified current identity or refuses honestly',
`areaId=${bagmati?.areaId} name=${bagmati?.name} sourceIdentity=${bagmati?.sourceIdentity} source=${bagmati?.source} km2=${bagmati?.areaKm2} ms=${timings.bagmatiMs} code=${bagmati?.code || ''} ${bagmati?.error || ''}`,
);
await settle(3000);
await shot('bagmati-outline');
t0 = Date.now();
let r = await run('resolve_area', { query: 'Bagmati province' });
report(
bagmatiVerified
? r?.ok === true &&
r?.cached === true &&
r?.ms < 300 &&
r?.sourceIdentity === 'Bagmati Province'
: bagmatiRefused &&
r?.ok === false &&
r?.code === bagmati?.code &&
!r?.areaId,
'geometry: a repeated Bagmati request reuses only verified identity, never refused geometry',
`ok=${r?.ok} code=${r?.code || ''} areaId=${r?.areaId || ''} sourceIdentity=${r?.sourceIdentity || ''} cached=${r?.cached} ms=${r?.ms} roundTrip=${Date.now() - t0}`,
);
// A city no bundled pack holds: the guarded Nominatim outline route.
t0 = Date.now();
const kathmandu = await run('resolve_area', { query: 'Kathmandu', draw: true, flyTo: false });
timings.kathmanduMs = Date.now() - t0;
report(kathmandu?.ok === true && kathmandu?.drawn === true && kathmandu?.source === 'osm' && kathmandu?.areaKm2 > 10 && kathmandu?.areaKm2 < 2000,
'geometry: "outline Kathmandu" comes from the guarded Nominatim outline',
`areaId=${kathmandu?.areaId} source=${kathmandu?.source} km2=${kathmandu?.areaKm2} parts=${kathmandu?.parts} ms=${timings.kathmanduMs} err=${kathmandu?.code || ''} ${kathmandu?.error || ''}`);
await settle(2500);
await shot('kathmandu-outline');
r = await run('resolve_area', { query: 'Georgia' });
report(r?.ok === false && r?.needsClarification === true && (r?.candidates || []).length >= 2,
'geometry: an ambiguous area asks with candidates instead of guessing',
`candidates=${(r?.candidates || []).map((c) => `${c.name}/${c.level}/${c.country || ''}`).join(', ')}`,
);
if (bagmatiVerified) {
await run('set_layer_visibility', {
layerId: 'local-firms',
enabled: true,
});
let fires = null;
for (let attempt = 0; attempt < 10; attempt += 1) {
fires = await run('analyst_query', {
layers: ['local-firms'],
scope: { kind: 'area', areaId: bagmati.areaId },
});
if (fires?.ok || !['NOT_READY', 'LAYER_OFF'].includes(fires?.code)) break;
await settle(4000);
}
if (fires?.code === 'FEED_UNAVAILABLE') {
skipped(
'geometry: "count fires inside it" counts inside the verified Bagmati area',
`fires feed unavailable: ${fires?.error}`,
);
} else {
report(
fires?.ok === true &&
Number.isFinite(fires?.count) &&
/Bagmati/i.test(fires?.scopeLabel || '') &&
fires?.coverage?.scope === `area:${bagmati.areaId}`,
'geometry: "count fires inside it" counts inside the verified Bagmati area',
`count=${fires?.count} scopeLabel="${fires?.scopeLabel}" scope=${fires?.coverage?.scope} err=${fires?.error || ''}`,
);
}
await run('set_layer_visibility', {
layerId: 'local-firms',
enabled: false,
});
} else {
skipped(
'geometry: fire count waits for a verified Bagmati area',
`boundary refused with ${bagmati?.code || 'unknown error'}`,
);
}
// (G2) "Count flights inside the area I drew": the draw tool's own spec.
await teleport(30.27, -97.74, 600_000);
await run('set_layer_visibility', { layerId: 'flights', enabled: true });
for (let attempt = 0; attempt < 12; attempt += 1) {
r = await run('analyst_query', { layers: ['flights'], scope: { kind: 'anywhere' } });
if (r?.ok && r.count > 0) break;
await settle(5000);
}
const drawnBox = [[-99, 29], [-96.5, 29], [-96.5, 31.5], [-99, 31.5], [-99, 29]];
await page.evaluate((ring) => window.__gevAnnotations.annotate([{ type: 'area', manual: true, ring, label: 'QA drawn box' }]), drawnBox);
r = await run('analyst_query', { layers: ['flights'], scope: { kind: 'drawn' } });
const loaded = await run('analyst_query', { layers: ['flights'], scope: { kind: 'anywhere' } });
report(r?.ok === true && r?.scopeLabel === 'inside QA drawn box' && Number.isFinite(r?.count) && r.count <= (loaded?.count ?? Infinity),
'geometry: "count flights inside the area I drew" counts inside the drawn outline',
`count=${r?.count} of ${loaded?.count} loaded scopeLabel="${r?.scopeLabel}" err=${r?.error || ''}`);
await settle(2500);
await shot('drawn-area-count');
// Thousands of aircraft starve a SwiftShader page; the landmark lookup
// below has a 10 s budget.
await run('set_layer_visibility', { layerId: 'flights', enabled: false });
await run('clear_annotations', {});
// (G3) "Draw an area around the Ferry Building": the enclosing mapped area
// (Nominatim grounds, then the OpenFreeMap area holding it), else an
// approximate buffer — either way a drawn, reusable area.
await teleport(37.7955, -122.3937, 2500, -60);
await settle(1500);
t0 = Date.now();
const ferry = await run('resolve_area', { query: 'Ferry Building, San Francisco', around: true, flyTo: false });
timings.ferryMs = Date.now() - t0;
const ferryNear = Array.isArray(ferry?.bbox) && Math.abs(ferry.bbox[1] - 37.795) < 0.05 && Math.abs(ferry.bbox[0] + 122.394) < 0.05;
report(ferry?.ok === true && ferry?.drawn === true && ferryNear && (ferry?.approximate === true || ['osm', 'openfreemap'].includes(ferry?.source)),
'geometry: "draw an area around the Ferry Building" draws a reusable landmark area',
`areaId=${ferry?.areaId} source=${ferry?.source} approximate=${ferry?.approximate} basis=${ferry?.display?.basis} km2=${ferry?.areaKm2} ms=${timings.ferryMs} err=${ferry?.error || ''}`);
await settle(3000);
await shot('ferry-building-area');
// (G4) "Find recent imagery of this area": the Recent Imagery catalog for
// that area, the best acquisition pinned through the layer itself.
t0 = Date.now();
r = await run('find_imagery', { areaId: ferry?.areaId || 'missing' });
timings.imageryMs = Date.now() - t0;
const pinned = await page.evaluate(() => window.__godsEyeView.dataManager.layers.get('recent-imagery')?.module?.getSnapshot?.().pins?.a?.key || null);
if (r?.code === 'CATALOG_UNAVAILABLE' || r?.code === 'SEARCH_TIMEOUT') {
skipped('geometry: "find recent imagery of this area" pins the best acquisition', `catalog: ${r?.error}`);
} else {
report(r?.ok === true && (r?.count === 0 ? r?.shown === null : Boolean(r?.shown?.date) && String(pinned).endsWith(`:${r.shown.date}`)),
'geometry: "find recent imagery of this area" pins the best acquisition',
`count=${r?.count} shown=${JSON.stringify(r?.shown)} pinned=${pinned} ms=${timings.imageryMs} err=${r?.error || ''}`);
}
await settle(4000);
await shot('recent-imagery-area');
await run('set_layer_visibility', { layerId: 'recent-imagery', enabled: false });
// (G5) Bulk place search without an operator Overpass: a plain refusal.
const configured = await page.evaluate(() => fetch('/api/overpass/status').then((res) => res.json()).catch(() => null));
r = await run('osm_query', { what: 'hospitals' });
if (configured?.configured) {
skipped('geometry: place search without an operator Overpass is refused', 'this server has OVERPASS_UPSTREAMS');
} else {
report(r?.ok === false && r?.code === 'OVERPASS_NOT_CONFIGURED',
'geometry: place search without an operator Overpass is refused', `code=${r?.code} error=${r?.error}`);
}
const nominatimUsed = nominatimCount() - nominatimStart;
const overpass = overpassRequests.slice(overpassStart);
console.log(` geometry timings: ${JSON.stringify(timings)}; public Nominatim requests: ${nominatimUsed}`);
report(overpass.length === 0, 'geometry: no Overpass request', overpass.slice(0, 3).join(' | ') || 'none');
report(nominatimUsed <= 15, 'geometry: public Nominatim requests within budget (15)', `used=${nominatimUsed}`);
await run('clear_annotations', {});
}
const NOMINATIM_STATE = path.join(ROOT, '.gev-cache', 'nominatim', 'state.json');
function nominatimCount() {
try {
+333
View File
@@ -0,0 +1,333 @@
#!/usr/bin/env node
/** Real-app zoom and tilt regression. Start a dev server, then run this file. */
import assert from 'node:assert/strict';
import fs from 'node:fs';
import path from 'node:path';
import puppeteer from 'puppeteer';
const origin = process.env.QA_BASE_URL || 'http://localhost:4173';
const browser = await puppeteer.launch({
headless: true,
args: [
'--no-sandbox',
...(process.platform === 'darwin'
? ['--use-angle=metal', '--enable-gpu']
: [
'--use-gl=angle',
'--use-angle=swiftshader',
'--enable-unsafe-swiftshader',
]),
],
protocolTimeout: 120_000,
});
const errors = [];
try {
const page = await browser.newPage();
page.on('pageerror', (error) => errors.push(error.message));
await page.setViewport({ width: 1200, height: 800 });
await page.goto(`${origin}/?welcome=0`, { waitUntil: 'domcontentloaded' });
await page.waitForFunction(
() =>
window.__godsEyeView?.viewer &&
window.__godsEyeView.viewer.scene.screenSpaceCameraController
.enableInputs &&
document.getElementById('loading-screen')?.classList.contains('hidden'),
{ timeout: 90_000 },
);
const renderer = await page.evaluate(async () => {
const C = await import('/node_modules/cesium/Build/Cesium/index.js');
const { maxPitchForHeight } = await import('/src/app/zoomOutGuard.js');
const { readCameraTargetFrame } =
await import('/src/ui/cameraOrientationControls.js');
const viewer = window.__godsEyeView.viewer;
viewer.camera.cancelFlight();
viewer.trackedEntity = undefined;
// Explicit real renders make each gesture independent of host frame rate.
viewer.useDefaultRenderLoop = false;
const controller = viewer.scene.screenSpaceCameraController;
controller.inertiaZoom = 0;
controller.inertiaSpin = 0;
const original = viewer.camera.setView.bind(viewer.camera);
const qa = {
viewer,
guardWrites: 0,
targetPitch: () => readCameraTargetFrame(viewer)?.pitch,
history: [],
frame() {
viewer.scene.requestRender();
viewer.render();
qa.history.push({
height: viewer.camera._positionCartographic.height,
guardWrites: qa.guardWrites,
position: { ...viewer.camera.position },
});
if (qa.history.length > 80) qa.history.shift();
},
pose() {
return {
height: viewer.camera.positionCartographic.height,
pitch: viewer.camera.pitch,
maxPitch: maxPitchForHeight(
viewer.camera.positionCartographic.height,
),
guardWrites: qa.guardWrites,
};
},
reset(height = 1_000_000, pitch = -35) {
viewer.camera.cancelFlight();
viewer.camera.lookAtTransform(C.Matrix4.IDENTITY);
original({
destination: C.Cartesian3.fromDegrees(0, 0, height),
orientation: { heading: 0, pitch: C.Math.toRadians(pitch), roll: 0 },
});
qa.frame();
qa.guardWrites = 0;
},
};
viewer.camera.setView = (options) => {
// Cesium itself also calls setView while processing touch movement.
if (new Error().stack.includes('/src/app/zoomOutGuard.js'))
qa.guardWrites++;
return original(options);
};
window.__zoomGuardQa = qa;
const gl = viewer.scene.context._gl;
const debug = gl.getExtension('WEBGL_debug_renderer_info');
return gl.getParameter(debug ? debug.UNMASKED_RENDERER_WEBGL : gl.RENDERER);
});
console.log(`[INFO] WebGL renderer: ${renderer}`);
const frame = (count = 1) =>
page.evaluate(async (n) => {
for (let i = 0; i < n; i++) {
window.__zoomGuardQa.frame();
await new Promise((resolve) => setTimeout(resolve, 0));
}
return window.__zoomGuardQa.pose();
}, count);
const reset = (height, pitch) =>
page.evaluate((h, p) => window.__zoomGuardQa.reset(h, p), height, pitch);
const session = await page.createCDPSession();
for (const viewport of [
{ width: 1200, height: 800 },
{ width: 390, height: 844 },
]) {
await page.setViewport(viewport);
await page.evaluate(() => window.__zoomGuardQa.viewer.resize());
const x = Math.round(viewport.width / 2);
const y = Math.round(viewport.height / 2);
await reset(1_000_000, -25);
await page.mouse.move(x, y);
await page.evaluate(
({ x, y }) => {
window.__zoomGuardQa.viewer.canvas.dispatchEvent(
new WheelEvent('wheel', {
deltaY: 2400,
clientX: x,
clientY: y,
bubbles: true,
cancelable: true,
}),
);
},
{ x, y },
);
const first = await frame();
console.log(`[INFO] ${viewport.width}px wheel delivered`);
const settled = await frame(40);
assert.ok(first.height > 1_000_000, 'wheel must move the real camera');
assert.ok(settled.guardWrites > 0, 'zoom guard must run');
assert.ok(
settled.pitch <= settled.maxPitch + 1e-6,
`stationary pitch must finish easing: ${JSON.stringify(settled)}`,
);
for (let i = 0; i < 12; i++) {
await page.evaluate(
({ x, y }) => {
window.__zoomGuardQa.viewer.canvas.dispatchEvent(
new WheelEvent('wheel', {
deltaY: 2400,
clientX: x,
clientY: y,
bubbles: true,
cancelable: true,
}),
);
},
{ x, y },
);
await frame();
}
const ceiling = await frame(40);
assert.ok(ceiling.height <= 40_000_001, 'user zoom respects the ceiling');
assert.ok(
ceiling.height > 39_000_000,
'exercise the ceiling, not a low orbit',
);
assert.ok(
ceiling.pitch <= ceiling.maxPitch + 1e-6,
`ceiling must settle: ${JSON.stringify(await page.evaluate(() => window.__zoomGuardQa.history))}`,
);
console.log(
`[PASS] ${viewport.width}px wheel, stationary easing and ceiling`,
);
if (process.env.QA_SCREENSHOT_DIR) {
fs.mkdirSync(process.env.QA_SCREENSHOT_DIR, { recursive: true });
await page.screenshot({
path: path.join(
process.env.QA_SCREENSHOT_DIR,
`zoom-ceiling-${viewport.width}.png`,
),
});
}
await reset();
await page.mouse.move(x, y);
await page.keyboard.down('Control');
await page.mouse.down({ button: 'right' });
for (let i = 1; i <= 6; i++) {
await page.mouse.move(x, y + i * 3);
await frame();
}
await page.mouse.up({ button: 'right' });
await page.keyboard.up('Control');
const tilted = await frame(3);
assert.equal(
tilted.guardWrites,
0,
'Ctrl+right tilt must not trigger guard',
);
assert.ok(Math.abs(tilted.height - 1_000_000) > 1, 'tilt must move camera');
await reset();
const touches = (dy) => [
{ x: x - 60, y: y + dy, id: 1 },
{ x: x + 60, y: y + dy, id: 2 },
];
await session.send('Input.dispatchTouchEvent', {
type: 'touchStart',
touchPoints: touches(0),
});
await frame();
for (let i = 1; i <= 6; i++) {
await session.send('Input.dispatchTouchEvent', {
type: 'touchMove',
touchPoints: touches(i * 3),
});
await frame();
}
await session.send('Input.dispatchTouchEvent', {
type: 'touchEnd',
touchPoints: [],
});
const translated = await frame(3);
assert.equal(
translated.guardWrites,
0,
'parallel two-finger tilt must not trigger guard',
);
assert.ok(
Math.abs(translated.height - 1_000_000) > 1,
'touch tilt must move camera',
);
console.log(
`[PASS] ${viewport.width}px Ctrl+right and parallel touch tilt`,
);
await reset(1_000_000, -35);
await page.evaluate(
({ x, y }) => {
window.__zoomGuardQa.viewer.canvas.dispatchEvent(
new WheelEvent('wheel', {
deltaY: 120,
clientX: x,
clientY: y,
bubbles: true,
cancelable: true,
}),
);
},
{ x, y },
);
const beforeTilt = await frame();
assert.ok(
beforeTilt.guardWrites > 0,
'wheel correction must be pending before UI tilt',
);
const targetPitchBeforeClick = await page.evaluate(() =>
window.__zoomGuardQa.targetPitch(),
);
assert.ok(
Number.isFinite(targetPitchBeforeClick),
'UI tilt fixture must have a ground target under the canvas center',
);
// Programmatic fixture resets precede moveEnd, so the button's published
// aria state may still describe the previous view. The action toggles the
// current target frame: angles shallower than -60 degrees are tilted.
const wasTilted = targetPitchBeforeClick > -Math.PI / 3;
await page.click('#tilt-map-view');
const uiTilt = await page.evaluate(
async (desiredDegrees) => {
const qa = window.__zoomGuardQa;
const desired = (desiredDegrees * Math.PI) / 180;
const started = performance.now();
const deadline = started + 3000;
while (performance.now() < deadline) {
qa.frame();
// The 650 ms animator can be numerically near its destination
// before it releases preUpdate. Finish its lifetime before resetting.
if (
performance.now() - started >= 700 &&
Math.abs(qa.targetPitch() - desired) < 1e-5
)
return { settled: true, guardWrites: qa.guardWrites };
await new Promise((resolve) => setTimeout(resolve, 16));
}
return {
settled: false,
guardWrites: qa.guardWrites,
pitch: qa.targetPitch(),
};
},
wasTilted ? -89 : -35,
);
assert.ok(
uiTilt.settled,
`UI tilt must reach its destination: ${JSON.stringify(uiTilt)}`,
);
assert.equal(
uiTilt.guardWrites,
beforeTilt.guardWrites,
'UI navigation handoff must cancel pending zoom correction',
);
console.log(
`[PASS] ${viewport.width}px UI tilt takes over a pending wheel correction`,
);
await reset(1_000_000, -25);
await session.send('Input.dispatchTouchEvent', {
type: 'touchStart',
touchPoints: touches(0),
});
await frame();
await session.send('Input.dispatchTouchEvent', {
type: 'touchMove',
touchPoints: [
{ x: x - 35, y, id: 1 },
{ x: x + 35, y, id: 2 },
],
});
await frame();
await session.send('Input.dispatchTouchEvent', {
type: 'touchEnd',
touchPoints: [],
});
const pinched = await frame(40);
assert.ok(pinched.height > 1_000_000, 'pinch-out must move camera outward');
assert.ok(pinched.guardWrites > 0, 'pinch-out must trigger the guard');
assert.ok(pinched.pitch <= pinched.maxPitch + 1e-6);
console.log(`[PASS] ${viewport.width}px pinch-out settles below the limb`);
}
assert.deepEqual(errors, [], 'no JavaScript page errors');
console.log('[PASS] no JavaScript page errors');
} finally {
await browser.close();
}
+59
View File
@@ -0,0 +1,59 @@
import { spawnSync } from 'node:child_process';
import path from 'node:path';
import { pathToFileURL } from 'node:url';
import {
discoverUnitTestFiles,
UNIT_TEST_NODE_FLAGS,
} from './run-unit-tests.mjs';
/** Discover every layer/provider benchmark, including those added by other branches. */
export function discoverPerfTestFiles(root = process.cwd()) {
return discoverUnitTestFiles(root).filter((file) =>
file.endsWith('.perf.test.mjs'),
);
}
/** Shared by benchmarks so normal unit runs visibly skip timing assertions. */
export function perfBudgetTestOptions(env = process.env) {
return {
skip:
env.GEV_PERF_BUDGETS === '1'
? false
: 'Timing budgets are opt-in; run npm run test:perf (GEV_PERF_BUDGETS=1). Deterministic delta/write checks still run in the normal suite.',
};
}
/** Build a serial invocation so benchmarks do not compete for CPU time. */
export function perfTestArgs(files) {
// A single serial test process keeps benchmark files from competing with
// each other; the normal suite still discovers them and reports their skips.
return [
...UNIT_TEST_NODE_FLAGS,
'--test',
'--test-concurrency=1',
'--test-isolation=none',
...files,
];
}
/** Run discovered benchmarks with wall-clock budgets enabled. */
export function runPerfTests({
root = process.cwd(),
env = process.env,
run = spawnSync,
} = {}) {
const files = discoverPerfTestFiles(root);
if (!files.length) throw new Error('No *.perf.test.mjs benchmarks found');
const result = run(process.execPath, perfTestArgs(files), {
cwd: root,
stdio: 'inherit',
env: { ...env, GEV_PERF_BUDGETS: '1' },
});
if (result.error) throw result.error;
return result.status ?? 1;
}
const invoked = process.argv[1]
? pathToFileURL(path.resolve(process.argv[1])).href
: '';
if (import.meta.url === invoked) process.exitCode = runPerfTests();
+9
View File
@@ -409,6 +409,15 @@ async function main() {
status: 'ready',
}));
}
// CelesTrak SATCAT, which the satellite classes read beside the TLEs.
// An empty CSV (header only) keeps the run hermetic: the classes fall
// back to names and groups, as they do when SATCAT is unavailable.
if (isAppRequest && url.pathname.startsWith('/api/celestrak-satcat/')) {
return Promise.resolve(new Response(
'OBJECT_NAME,OBJECT_ID,NORAD_CAT_ID,OBJECT_TYPE,OPS_STATUS_CODE,LAUNCH_DATE\n',
{ status: 200, headers: { 'Content-Type': 'text/csv' } },
));
}
// CelesTrak TLE groups. Served from a fixed two-satellite catalog so
// the satellite assertions are deterministic and the run never depends
// on an upstream that rate-limits (it was answering 403 the night this
+50 -15
View File
@@ -90,8 +90,11 @@ export const CORE_PHRASES = Object.freeze([
// — annotations —
{ phrase: 'Annotate the Texas State Capitol and its grounds', expect: 'annotate_map', typedArgs: [where('annotate_map', (a) => (a.annotations || []).some((x) => x.type === 'area'))] },
{ phrase: 'Outline the state of Texas', expect: 'annotate_map', typedArgs: [where('annotate_map', (a) => (a.annotations || []).some((x) => x.type === 'area'))] },
{ phrase: 'Outline Lady Bird Lake', expect: 'annotate_map', typedArgs: [where('annotate_map', (a) => (a.annotations || []).some((x) => x.type === 'area'))] },
// A named boundary outlines through resolve_area (a reusable handle the
// user can count or search inside); annotate_map is for marks.
{ phrase: 'Outline the state of Texas', expect: 'resolve_area', args: { draw: true }, typedArgs: [where('resolve_area', (a) => a.draw === true)] },
// A lake is a water footprint as much as a boundary: either tool draws it.
{ phrase: 'Outline Lady Bird Lake', expect: { oneOf: ['annotate_map', 'resolve_area'] }, typedArgs: [(c) => c.name === 'resolve_area' || (c.name === 'annotate_map' && (c.args?.annotations || []).some((x) => x.type === 'area'))] },
{ phrase: 'Draw the walking route from the Capitol to Zilker Park', expect: 'annotate_map', typedArgs: [where('annotate_map', (a) => (a.annotations || []).some((x) => x.type === 'route' && (x.mode ?? 'walking') === 'walking'))] },
{ phrase: 'How far is the Eiffel Tower from the Louvre?', expect: 'annotate_map', typedArgs: [where('annotate_map', (a) => (a.annotations || []).some((x) => x.type === 'arrow'))] },
{ phrase: 'Clear the map', expect: 'clear_annotations' },
@@ -219,7 +222,7 @@ export const COVERAGE_PROBE = Object.freeze([
},
{
id: 'recent-imagery',
v2: { supported: true, correct: [[call('set_layer_visibility', { layerId: 'recent-imagery', enabled: true })]] },
v2: { supported: true, correct: [[call('find_imagery')], [call('set_layer_visibility', { layerId: 'recent-imagery', enabled: true })]] },
utterance: 'Show recent satellite imagery of this area',
scene: 'Austin',
expect: 'enable recent-imagery for current view (no tool today) — must NOT toggle satellites data layer or silently switch basemap',
@@ -250,13 +253,8 @@ export const COVERAGE_PROBE = Object.freeze([
},
{
id: 'fires-in-drawn-area',
// There is no drawn-area scope yet; the prompt says to say so. Any count
// presented as inside the drawn area is a faked capability.
v2: {
supported: false,
correct: [],
hallucinated: [where('analyst_query', (a) => (a.layers || []).includes('local-firms'))],
},
// The drawn-area scope exists: a count must be scoped to it.
v2: { supported: true, correct: [[where('analyst_query', (a) => (a.layers || []).includes('local-firms') && ['drawn', 'annotation', 'area'].includes(a.scope?.kind))]] },
utterance: 'Count the fires inside the area I drew',
scene: 'Los Angeles, fires layer on, an area drawn around Angeles National Forest',
expect: 'analyst_query local-firms scoped to the drawn polygon (no polygon scope today)',
@@ -548,14 +546,42 @@ export function dialogueResult(dialogue, toolName, turn) {
return byTurn[turn] ?? null;
}
// ── New capabilities (point-and-ask, layer reach) ─────────────────────────
// ── New capabilities (voice geometry, point-and-ask, layer reach) ─────────
//
// ROUTING_PHRASES is the qa-voice-routing table, so that harness runs exactly
// its own table. The benchmark runs CAPABILITY_EXTRAS as the separate
// `capability` suite so the core suite stays comparable across tool versions.
// ROUTING_PHRASES is the qa-voice-routing table (core + geometry + place
// search), so that harness runs exactly its own table. The benchmark runs
// CAPABILITY_EXTRAS as the separate `capability` suite so the core suite
// stays comparable across tool versions.
/** Areas, drawn-area counts and imagery: offered in every session. */
export const GEOMETRY_PHRASES = Object.freeze([
{ phrase: 'Outline Bagmati province', expect: 'resolve_area', args: { query: 'bagmati', draw: true } },
{ phrase: 'Outline Kathmandu district', expect: 'resolve_area', args: { query: 'kathmandu' } },
{ phrase: 'Show me the border of Punjab', expect: 'resolve_area', args: { query: 'punjab' } },
{ phrase: 'Draw an area around the Ferry Building in San Francisco', expect: 'resolve_area', args: { around: true } },
{ phrase: 'Count the fires inside Bagmati province', expect: { oneOf: ['analyst_query', 'resolve_area'] } },
{ phrase: 'How many flights are inside the area I drew?', expect: 'analyst_query', typedArgs: [where('analyst_query', (a) => a.scope?.kind === 'drawn')] },
{ phrase: 'How many ships are inside the area you just outlined?', expect: 'analyst_query', typedArgs: [where('analyst_query', (a) => a.scope?.kind === 'annotation' || a.scope?.kind === 'area')] },
{ phrase: 'Show me recent satellite imagery of this area', expect: 'find_imagery' },
{ phrase: 'Find the latest cloud-free satellite image here', expect: 'find_imagery', typedArgs: [where('find_imagery', (a) => typeof a.maxCloudPct === 'number' && a.maxCloudPct <= 20)] },
{ phrase: 'Get me recent imagery of Kathmandu', expect: { oneOf: ['find_imagery', 'resolve_area'] } },
]);
/**
* Bulk place search: osm_query is offered only when the server has an
* operator Overpass, so the routing harness skips these otherwise.
*/
export const OVERPASS_PHRASES = Object.freeze([
{ phrase: 'Show me all the hospitals in Kathmandu', expect: { oneOf: ['resolve_area', 'osm_query'] }, requires: ['osm_query'] },
{ phrase: 'Find the police stations in view', expect: 'osm_query', args: { what: 'police' }, requires: ['osm_query'] },
]);
/** The full qa-voice-routing table. */
export const ROUTING_PHRASES = CORE_PHRASES;
export const ROUTING_PHRASES = Object.freeze([
...CORE_PHRASES,
...GEOMETRY_PHRASES,
...OVERPASS_PHRASES,
]);
// Pointer context items in the app's own format (src/voice/pointerContext.js
// output), sent as an inert system item just before the user's words.
@@ -569,6 +595,7 @@ const GROUND_POINTER = {
ground: { lat: 30.2849, lon: -97.7341 }, screen: { x: 0.427, y: 0.547 },
};
const isPointer = (v) => String(v || '').toLowerCase() === 'pointer';
const bagmati = { ok: true, areaId: 'area-1', name: 'Bagmati Province', level: 'admin1', drawn: true, say: 'Bagmati Province — about 20,000 km²' };
const flightsInView = {
ok: true,
count: 3,
@@ -593,6 +620,14 @@ export const CAPABILITY_EXTRAS = Object.freeze([
{ phrase: 'Turn on the weather radar', expect: 'set_layer_visibility', typedArgs: [where('set_layer_visibility', (a) => a.layerId === 'weather-radar' && a.enabled === true)] },
{ phrase: 'Are there any hurricanes right now?', expect: { oneOf: ['analyst_query', 'set_layer_visibility'] }, typedArgs: [(c) => (c.args?.layers || []).includes('weather-cyclones') || c.args?.layerId === 'weather-cyclones'] },
{ phrase: 'Which bus is closest to the middle of the screen?', expect: 'analyst_query', typedArgs: [where('analyst_query', (a) => (a.layers || []).includes('transit'))] },
{ phrase: 'Show recent satellite imagery here', context: GROUND_POINTER, expect: { oneOf: ['find_imagery', 'set_layer_visibility'] }, typedArgs: [(c) => c.name === 'find_imagery' || (c.name === 'set_layer_visibility' && c.args?.layerId === 'recent-imagery')] },
{
id: 'bagmati-then-fires',
turns: ['Outline Bagmati province', 'Count the fires inside it'],
results: { resolve_area: [bagmati] },
first: { expect: 'resolve_area' },
second: { expect: 'analyst_query', typedArgs: [where('analyst_query', (a) => (a.layers || []).includes('local-firms') && a.scope?.kind === 'area' && a.scope?.areaId === 'area-1')] },
},
{
id: 'flights-then-second',
turns: ['How many flights are in view?', 'Track the second one'],