From b681d094ca0e2d0ea0d2cea0b0a6636410ea5e94 Mon Sep 17 00:00:00 2001 From: dajiaohuang Date: Tue, 29 Sep 2026 10:43:30 +0800 Subject: [PATCH 1/9] docs: correct ADSBDB cache persistence note --- src/layers/flights/policy.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/layers/flights/policy.js b/src/layers/flights/policy.js index f8358c98f..87e4617ee 100644 --- a/src/layers/flights/policy.js +++ b/src/layers/flights/policy.js @@ -213,8 +213,8 @@ export const COURSE_SLEW_DT_MAX_SEC = 0.25; // adsbdb enrichment (best-effort, fail-silent). Bounded fan-out: max 4 // concurrent requests, dispatches dripped ≥ENRICH_DISPATCH_GAP_MS apart // (≤5/s — adsbdb is a free community API; the dev-server proxy additionally -// caches per-key on disk forever, negative results included, so repeat -// sessions never re-hit adsbdb). Each key is requested at most once per +// caches successful and 404 responses per key for 24 hours, so later sessions +// may refresh them). Each key is requested at most once per // session. Priority jobs (tracked plane, model-eligible planes) jump the // queue; the ambient fleet sweep (below) fills the back at poll cadence. // --------------------------------------------------------------------------- From ac62542641d47f06330f56398b2d0e5f75a9b041 Mon Sep 17 00:00:00 2001 From: Honorius Neogy Date: Sat, 3 Oct 2026 15:03:31 -0400 Subject: [PATCH 2/9] fix(satellites): decode Alpha-5 catalog numbers in the shared TLE module Catalog numbers above 99999 arrive in a TLE as Alpha-5: a letter A-Z, skipping I and O, stands for 10-33 in the ten-thousands place, so 100000 is A0000. satellite.js keeps the field as the string satrec.satnum, and the three catalog builders converted it with Number(), which gives NaN; the Set and Map used to deduplicate and store treat NaN as equal to NaN, so the first such satellite was stored under NaN and every later one was dropped as a duplicate (#751). The satellite tools read the same field with parseInt and reported no number, so such a satellite could not be found by it. The decoder lives in src/sources/tle.js, beside parseTleText, which the layer and the tools both import already: noradIdFromSatnum() for the three builders, and tleCatalogNumber() decodes through the same function. Every other field reads as it did before: Number() in the layer, parseInt or null in the tools. --- src/layers/satellites/catalog.js | 3 +- src/layers/satellites/ingestion.js | 3 +- src/layers/satellites/orbits.js | 4 +-- src/sources/tle.js | 44 ++++++++++++++++++++++++- src/tools/queries/space.test.mjs | 52 +++++++++++++++++++++++++++++- 5 files changed, 100 insertions(+), 6 deletions(-) diff --git a/src/layers/satellites/catalog.js b/src/layers/satellites/catalog.js index 25eaf9b2c..fd136c82d 100644 --- a/src/layers/satellites/catalog.js +++ b/src/layers/satellites/catalog.js @@ -1,5 +1,6 @@ import * as Cesium from 'cesium'; import { twoline2satrec } from 'satellite.js'; +import { noradIdFromSatnum } from '../../sources/tle.js'; import { DENSE_REFRESH_FRAMES, DENSE_GROUP_PATH, @@ -113,7 +114,7 @@ export function createCatalog({ state: layerState, services, parts, source }) { const entry = entries[i]; const satrec = twoline2satrec(entry.line1, entry.line2); if (!satrec || satrec.error !== 0) continue; - const noradId = Number(satrec.satnum); + const noradId = noradIdFromSatnum(satrec.satnum); if (layerState._catalog.has(noradId)) continue; // core catalog keeps priority const pos = parts.orbits.propagatePosition(satrec, now); if (!pos) continue; diff --git a/src/layers/satellites/ingestion.js b/src/layers/satellites/ingestion.js index 7fb36bb00..14e464892 100644 --- a/src/layers/satellites/ingestion.js +++ b/src/layers/satellites/ingestion.js @@ -1,5 +1,6 @@ import { twoline2satrec } from 'satellite.js'; import * as Cesium from 'cesium'; +import { noradIdFromSatnum } from '../../sources/tle.js'; import { CATALOG_GROUPS, ISS_NORAD, POINT_STYLES } from './policy.js'; export function createIngestion({ @@ -108,7 +109,7 @@ export function createIngestion({ const satrec = twoline2satrec(entry.line1, entry.line2); if (!satrec || satrec.error !== 0) continue; - const noradId = Number(satrec.satnum); + const noradId = noradIdFromSatnum(satrec.satnum); if (seen.has(noradId)) continue; seen.add(noradId); diff --git a/src/layers/satellites/orbits.js b/src/layers/satellites/orbits.js index 32bb57cf9..0e4461e85 100644 --- a/src/layers/satellites/orbits.js +++ b/src/layers/satellites/orbits.js @@ -8,7 +8,7 @@ import { twoline2satrec, } from 'satellite.js'; import { findNextSatellitePass } from '../../data/satellitePass.js'; -import { parseTleText } from '../../sources/tle.js'; +import { noradIdFromSatnum, parseTleText } from '../../sources/tle.js'; import { ORBIT_PATH_STEPS, ISS_NORAD } from './policy.js'; export function createOrbits({ state: layerState, services, parts, source }) { @@ -284,7 +284,7 @@ export function createOrbits({ state: layerState, services, parts, source }) { const current = propagatePosition(satrec, referenceDate); if (!current) return null; return { - noradId: Number(satrec.satnum), + noradId: noradIdFromSatnum(satrec.satnum), name: String(name || '').trim(), current, periodSec: orbitalPeriodSeconds(satrec), diff --git a/src/sources/tle.js b/src/sources/tle.js index ac7aedc2c..f5fad48bd 100644 --- a/src/sources/tle.js +++ b/src/sources/tle.js @@ -21,8 +21,50 @@ export function parseTleText(text) { return result; } +/** + * The catalog number an Alpha-5 field stands for, or null for any other field. + * + * Since the catalog passed 99999 (July 2026) a TLE writes the numbers above it + * in Alpha-5: a letter A–Z, skipping I and O, stands for 10–33 in the + * ten-thousands place, so 100000 is "A0000" and 339999 is "Z9999". + */ +function alpha5CatalogNumber(field) { + const letter = field[0]; + if ( + letter >= 'A' && + letter <= 'Z' && + letter !== 'I' && + letter !== 'O' && + /^\d{4}$/.test(field.slice(1)) + ) { + let tens = letter.charCodeAt(0) - 65 + 10; + if (letter > 'I') tens -= 1; + if (letter > 'O') tens -= 1; + return tens * 10000 + Number(field.slice(1)); + } + return null; +} + +/** + * NORAD catalog number of a satrec as a number. + * + * satellite.js keeps the TLE's five-character field verbatim in + * `satrec.satnum`, and `Number("A0000")` is NaN, so every Alpha-5 satellite + * would share one NaN key. An Alpha-5 field decodes to its number; a + * five-digit field is unchanged; anything else stays NaN. + * @param {string} satnum + * @returns {number} + */ +export function noradIdFromSatnum(satnum) { + const field = String(satnum ?? '').trim(); + return alpha5CatalogNumber(field) ?? Number(field); +} + /** The NORAD catalog number from TLE line 1, or null. */ export function tleCatalogNumber(line1) { - const number = Number.parseInt(String(line1).slice(2, 7), 10); + const field = String(line1).slice(2, 7); + const alpha5 = alpha5CatalogNumber(field); + if (alpha5 !== null) return alpha5; + const number = Number.parseInt(field, 10); return Number.isInteger(number) ? number : null; } diff --git a/src/tools/queries/space.test.mjs b/src/tools/queries/space.test.mjs index f6c2a8df8..5ff267ce1 100644 --- a/src/tools/queries/space.test.mjs +++ b/src/tools/queries/space.test.mjs @@ -1,7 +1,12 @@ import assert from 'node:assert/strict'; import test from 'node:test'; +import { twoline2satrec } from 'satellite.js'; import { composeCatalog, coreTools } from '../index.js'; -import { parseTleText, tleCatalogNumber } from '../../sources/tle.js'; +import { + noradIdFromSatnum, + parseTleText, + tleCatalogNumber, +} from '../../sources/tle.js'; // Element sets published for 1 January 2024. const TLE = `ISS (ZARYA) @@ -51,6 +56,51 @@ for (const missingLine of [1, 2]) { }); } +test('Alpha-5 catalog numbers decode, skipping I and O', () => { + for (const [field, id] of [ + ['A0000', 100000], + ['A0404', 100404], + ['H9999', 179999], + ['J0000', 180000], + ['N9999', 229999], + ['P0000', 230000], + ['T0449', 270449], + ['Z9999', 339999], + ]) { + assert.equal(noradIdFromSatnum(field), id, field); + assert.equal(tleCatalogNumber(`1 ${field}U`), id, field); + } +}); + +test('every other catalog field reads as it did before', () => { + assert.equal(noradIdFromSatnum('25544'), 25544); + assert.equal(noradIdFromSatnum('00404'), 404); + assert.equal(noradIdFromSatnum(''), 0); // as Number('') did + for (const field of ['I0000', 'O0000', 'a0404', 'A00']) { + assert.ok(Number.isNaN(noradIdFromSatnum(field)), field); + assert.equal(tleCatalogNumber(`1 ${field}U`), null, field); + } +}); + +test('a satellite numbered above 99999 keeps its number and is found by it', async () => { + // The ISS set with its catalog field in Alpha-5: A0000 is 100000. + const text = TLE.replaceAll('25544', 'A0000'); + const [iss] = parseTleText(text); + const { satnum } = twoline2satrec(iss.line1, iss.line2); + assert.ok(Number.isNaN(Number(satnum))); // what the catalog builders used + assert.equal(noradIdFromSatnum(satnum), 100000); + const catalog = composeCatalog({ + tools: coreTools, + services: { satellites: satellites(text), clock: CLOCK }, + }); + const result = await catalog.call('next_satellite_pass', { + location: { lat: 40.7, lon: -74 }, + satellite: '100000', + }); + assert.equal(result.data.satellite, 'ISS (ZARYA)'); + assert.equal(result.data.norad, 100000); +}); + test('the next pass defaults to the ISS and reports times, peak and direction', async () => { const catalog = composeCatalog({ tools: coreTools, From 6b70392f3fa8d0c984c8cbf693f2485263bc575d Mon Sep 17 00:00:00 2001 From: Honorius Neogy Date: Sat, 3 Oct 2026 15:03:32 -0400 Subject: [PATCH 3/9] docs: note Alpha-5 catalog numbers in the changelog and current state --- CHANGELOG.md | 8 ++++++++ docs/CURRENT-STATE.md | 1 + 2 files changed, 9 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index c885e2caa..213563788 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -386,6 +386,14 @@ needs only the Map Tiles API and the Maps JavaScript API (Street View). Keyless sessions and the Photon/Nominatim fallback order are unchanged. +- Read satellite catalog numbers above 99999. A TLE writes them in Alpha-5, + a letter for the first two digits (`A0000` is 100000), which the satellites + layer converted to `NaN`: every such satellite shared one catalog entry and + all but the first were dropped as duplicates. The satellite tools read the + same field as no number, so such a satellite could not be found by it. The + layer and the tools now decode the field in `src/sources/tle.js` + (Honorius Neogy, #767, fixes #751). + ## [0.2.1] — 2026-10-02 — God's Eye View through Your Agent (with security fixes and other features) - Ship each bundled data pack once. The region, marine, admin-boundary, diff --git a/docs/CURRENT-STATE.md b/docs/CURRENT-STATE.md index 2cc281812..15a4ec999 100644 --- a/docs/CURRENT-STATE.md +++ b/docs/CURRENT-STATE.md @@ -4139,6 +4139,7 @@ silently demoting every later lookup for the session. - **The DENSE chip reports the dense LOAD, not the catalog param.** The param flips synchronously while the Starlink shell takes seconds to arrive over a chunked load, and CelesTrak 502s that feed regularly. So the chip reads `DENSE ···` (busy, disabled) while loading, ACTIVE only once dense points are actually on screen, and `DENSE ✕` with the reason on hover when the load fails — a failure also reverts `catalog` to `core`, drops any partial chunk, and leaves the chip clickable to retry. A load is judged by points added, not by HTTP status: a 200 carrying an empty body, a passed-through HTML error page, or only TLEs the core catalog already owns fails with the same revert semantics as a 502. Any explicit request for `core` clears a latched error even when the mode does not change, so a Space Missions restore of an already-core snapshot never leaves the user with a failure they did not cause. Because the load settles asynchronously, the layer pushes a re-render through the optional `setRowControlsListener()` hook; nothing else would repaint that row before the 5-minute catalog refresh, so the count and legend would otherwise sit stale. - **A dependency owner takes the row with it.** Space Missions borrows this layer for TLE lookup with `showPoints:false`; while points are hidden the layer returns empty row controls, so the legend never describes an empty sky and the chip cannot accept a write that the owner's restore would silently revert. - The detection-overlay record cache (`_detectionObjects`) is cleared with the catalog on every rebuild: it stamps id/class at creation only, and a rebuild can re-tag a satellite when a partial CelesTrak outage changes which group wins dedupe. +- **Catalog numbers above 99999** arrive in a TLE as Alpha-5: a letter A–Z, skipping I and O, for the first two digits, so `A0000` is 100000 and `Z9999` is 339999. `src/sources/tle.js` decodes the field: `noradIdFromSatnum()` for the satellites layer, which keys and deduplicates its catalog by that number, and `tleCatalogNumber()` for the satellite tools. A five-digit field reads as before; a field that is neither stays `NaN` in the layer and `null` in the tools. - **CCTV estimated bearings:** a camera whose pack marks `headingConfidence: 'low'` (the id-hash `fallbackHeadingFromId` bearing) shows `HDG n° (ESTIMATED)` in the HUD and draws its coverage wireframe dashed; colours, widths and active/idle emphasis are unchanged. A manual calibration From 243e84150c50fa68d30cddd17169c6d9a2a81d18 Mon Sep 17 00:00:00 2001 From: wasif Date: Sat, 17 Oct 2026 19:53:02 +0400 Subject: [PATCH 4/9] docs: list every npm alias for the qa gates CONTRIBUTING.md said "Only a couple (`qa:map-source-tray`, `qa:transit`) have an `npm run` alias". package.json defines five: `qa:map-source-tray`, `qa:panel-resize`, `qa:street-level`, `qa:street-level:fixtures` and `qa:transit`. The two missing names are not incidental -- .github/workflows/ci.yml already invokes `npm run qa:street-level:fixtures` and `npm run qa:panel-resize`, so a contributor reading the guide is told to reach for `node scripts/.mjs` for gates the project itself runs through npm. Verified against package.json scripts and .github/workflows/ci.yml on main (1df2a27). --- CONTRIBUTING.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 6d6b968db..01a9ac374 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -54,8 +54,9 @@ head -40 scripts/qa-radio.mjs # what this runnable gate proves and how to run i Runnable gate entrypoints document what they assert and how to invoke them. Most need a dev server; some also need a specific provider key or port. Follow the entrypoint's header rather than assuming every matching file accepts the -same arguments. Only a couple (`qa:map-source-tray`, `qa:transit`) have an -`npm run` alias. +same arguments. Five have an `npm run` alias: `qa:map-source-tray`, +`qa:panel-resize`, `qa:street-level`, `qa:street-level:fixtures` and +`qa:transit`. If you aren't sure which gate covers your change, search `docs/CURRENT-STATE.md` for the feature: it names the gate for many of them, and it's the authoritative From 7828f6903852c370d1d0cf37af56157fdfa9aa74 Mon Sep 17 00:00:00 2001 From: Sameh Khamis Date: Sat, 17 Oct 2026 11:30:38 -0700 Subject: [PATCH 5/9] Name the two QA gates CI runs The CI callout said no qa-*.mjs gate runs in CI; the workflow runs the Street Level fixtures and panel-resize gates against the built preview. --- CONTRIBUTING.md | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 01a9ac374..ba0d38768 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -64,12 +64,13 @@ runtime reference either way. `scripts/qa-l9-matrix.mjs` aggregates the broader release-candidate checks and selected harnesses; it does not replace the focused gate for the feature you changed. -> **CI does not cover this for you.** The workflow runs the setup policy +> **CI covers only two of these for you.** The workflow runs the setup policy > checks, the formatting and package-boundary checks, the unit suite and the -> production build, plus a Windows onboarding job. It runs neither -> `npm run test:track` nor any `qa-*.mjs` gate — both need a live dev server -> and a browser. Include the applicable local run in your PR's validation -> evidence. +> production build, plus a Windows onboarding job, and against the built preview +> it runs `qa:street-level:fixtures` and `qa:panel-resize`. It does not run +> `npm run test:track` or any other `qa-*.mjs` gate — those need a live dev +> server and a browser. Include the applicable local run in your PR's +> validation evidence. ## Share-link layer tokens From 761953b0bf49907132406c1add8a95bd842daeda Mon Sep 17 00:00:00 2001 From: Sameh Khamis Date: Sat, 17 Oct 2026 11:30:46 -0700 Subject: [PATCH 6/9] Correct the remaining permanent-cache comments The adsbdb proxy keeps successful and 404 answers for 24 hours; two policy comments and the provider's doc comment still said forever. --- server/providers/aircraft/enrichment.js | 5 +++-- src/layers/flights/policy.js | 4 ++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/server/providers/aircraft/enrichment.js b/server/providers/aircraft/enrichment.js index 9b7c1b1bb..4c069fa7f 100644 --- a/server/providers/aircraft/enrichment.js +++ b/server/providers/aircraft/enrichment.js @@ -7,8 +7,9 @@ const ADSBDB_MAX_RESPONSE_BYTES = 1024 * 1024; /** * adsbdb.com enrichment proxy: callsign → route (airline + origin/destination * airports) and hex → aircraft type/registration. Free community API — cached - * aggressively: ONE upstream request per new key ever (404s negative-cached), - * persisted to disk so restarts don't re-hammer it. Adapted from skylight + * aggressively: successes and 404s are kept per key for 24 hours (TTL_MS) and + * persisted to disk so restarts don't re-hammer it; other failures are not + * cached. Adapted from skylight * (MIT) server/src/enrich/routes.ts. */ export function adsbdbProxy() { diff --git a/src/layers/flights/policy.js b/src/layers/flights/policy.js index 40cf8879a..2c015e6c9 100644 --- a/src/layers/flights/policy.js +++ b/src/layers/flights/policy.js @@ -243,7 +243,7 @@ export const ENRICH_DISPATCH_GAP_MS = 200; // of drip, so the backlog can't outgrow a poll and re-sorts fresh), and // - a ROLLING token-bucket budget (below) bounding the sustained ambient // request rate (repeat sessions resolve instantly from the proxy's -// permanent disk cache). +// 24-hour disk cache). // Fail-silent by contract: the sweep never throws into the poll loop, never // blocks rendering, and never touches tracking state. When a type answer // lands, _requestTypeEnrichment's callback swaps the billboard glyph + scale @@ -257,7 +257,7 @@ export const ENRICH_DISPATCH_GAP_MS = 200; // ENRICH_AMBIENT_REFILL_TOKENS every ENRICH_AMBIENT_REFILL_WINDOW_MS, clamped // at the ceiling (no banking beyond one bucket). Numbers: 150 / 5 min sustains // 0.5 req/s worst case — an order of magnitude under the 5/s drip that (with -// the 4-concurrent limit + the proxy's permanent disk cache) is the REAL +// the 4-concurrent limit + the proxy's 24-hour disk cache) is the REAL // politeness bound on adsbdb; the 300 ceiling preserves the old first-look // burst so a fresh region still classifies quickly. /** Bucket ceiling: max ambient tokens held at once (= the initial burst). */ From 7ec84ac316a4976ebffa2891fe69d5fe3cdbeede Mon Sep 17 00:00:00 2001 From: Sameh Khamis Date: Sat, 17 Oct 2026 11:40:30 -0700 Subject: [PATCH 7/9] Decode catalog numbers strictly and add the changelog fragment Route tleCatalogNumber() and noradIdFromSatnum() through one decoder that accepts only plain digits or a valid Alpha-5 field. parseInt and Number() read '2554A', '255 4', '-1234', '0x1F0', '1.5' and blank as numbers; they now return null/NaN. Digit satnums of any length still decode, so OMM satrecs numbered past 99999 keep working. Add a layer test that several Alpha-5 satellites survive ingestion dedupe, the changelog fragment for #767, and note in CURRENT-STATE that this is Alpha-5 TLE compatibility and malformed fields are rejected. --- changelog.d/767-alpha5-catalog-numbers.md | 11 ++++ docs/CURRENT-STATE.md | 2 +- src/layers/satellites/ingestion.test.mjs | 80 +++++++++++++++++++++++ src/sources/tle.js | 53 +++++++-------- src/tools/queries/space.test.mjs | 35 ++++++++-- 5 files changed, 147 insertions(+), 34 deletions(-) create mode 100644 changelog.d/767-alpha5-catalog-numbers.md create mode 100644 src/layers/satellites/ingestion.test.mjs diff --git a/changelog.d/767-alpha5-catalog-numbers.md b/changelog.d/767-alpha5-catalog-numbers.md new file mode 100644 index 000000000..8a96698a1 --- /dev/null +++ b/changelog.d/767-alpha5-catalog-numbers.md @@ -0,0 +1,11 @@ +- Read satellite catalog numbers written in Alpha-5, the TLE spelling of + numbers above 99999: a letter for the first two digits, skipping I and O + (`A0000` is 100000, `Z9999` is 339999). The satellites layer converted such a + field to `NaN`, so every Alpha-5 satellite shared one catalog entry and all + but the first were dropped as duplicates; the satellite tools read the same + field as no number, so such a satellite could not be found by it. The layer + and the tools now decode the field in `src/sources/tle.js`, and reject a + malformed one (a sign, decimal point, hex, embedded space or stray letter) + instead of reading part of it as a number. This is Alpha-5 TLE + compatibility: CelesTrak's TLE feed carries no satellite above 99999, which + needs the OMM feed (#1191) (Honorius Neogy, #767, fixes #751). diff --git a/docs/CURRENT-STATE.md b/docs/CURRENT-STATE.md index 82d93b458..dad629b7e 100644 --- a/docs/CURRENT-STATE.md +++ b/docs/CURRENT-STATE.md @@ -4324,7 +4324,7 @@ silently demoting every later lookup for the session. - **The DENSE chip reports the dense LOAD, not the catalog param.** The param flips synchronously while the Starlink shell takes seconds to arrive over a chunked load, and CelesTrak 502s that feed regularly. So the chip reads `DENSE ···` (busy, disabled) while loading, ACTIVE only once dense points are actually on screen, and `DENSE ✕` with the reason on hover when the load fails — a failure also reverts `catalog` to `core`, drops any partial chunk, and leaves the chip clickable to retry. A load is judged by points added, not by HTTP status: a 200 carrying an empty body, a passed-through HTML error page, or only TLEs the core catalog already owns fails with the same revert semantics as a 502. Any explicit request for `core` clears a latched error even when the mode does not change, so a Space Missions restore of an already-core snapshot never leaves the user with a failure they did not cause. Because the load settles asynchronously, the layer pushes a re-render through the optional `setRowControlsListener()` hook; nothing else would repaint that row before the 5-minute catalog refresh, so the count and legend would otherwise sit stale. - **A dependency owner takes the row with it.** Space Missions borrows this layer for TLE lookup with `showPoints:false`; while points are hidden the layer returns empty row controls, so the legend never describes an empty sky and the chip cannot accept a write that the owner's restore would silently revert. - The detection-overlay record cache (`_detectionObjects`) is cleared with the catalog on every rebuild: it stamps id/class at creation only, and a rebuild can re-tag a satellite when a partial CelesTrak outage changes which group wins dedupe. -- **Catalog numbers above 99999** arrive in a TLE as Alpha-5: a letter A–Z, skipping I and O, for the first two digits, so `A0000` is 100000 and `Z9999` is 339999. `src/sources/tle.js` decodes the field: `noradIdFromSatnum()` for the satellites layer, which keys and deduplicates its catalog by that number, and `tleCatalogNumber()` for the satellite tools. A five-digit field reads as before; a field that is neither stays `NaN` in the layer and `null` in the tools. +- **Catalog numbers above 99999** can be written in a TLE as Alpha-5: a letter A–Z, skipping I and O, for the first two digits, so `A0000` is 100000 and `Z9999` is 339999. CelesTrak's TLE feed carries no satellite above 99999 (that needs the OMM feed, #1191), so this is compatibility with Alpha-5 TLEs. `src/sources/tle.js` decodes the field through one shared helper: `noradIdFromSatnum()` for the satellites layer, which keys and deduplicates its catalog by that number, and `tleCatalogNumber()` for the satellite tools. Plain digits read as before (`noradIdFromSatnum()` accepts any length, as an OMM satrec carries the number unpadded); a malformed field — a sign, decimal point, hex, exponent, embedded space, stray or lowercase letter, or blank — is rejected rather than partly read, as `NaN` in the layer and `null` in the tools. - **CCTV estimated bearings:** a camera whose pack marks `headingConfidence: 'low'` (the id-hash `fallbackHeadingFromId` bearing) shows `HDG n° (ESTIMATED)` in the HUD and draws its coverage wireframe dashed; colours, widths and active/idle emphasis are unchanged. A manual calibration diff --git a/src/layers/satellites/ingestion.test.mjs b/src/layers/satellites/ingestion.test.mjs new file mode 100644 index 000000000..313d999ee --- /dev/null +++ b/src/layers/satellites/ingestion.test.mjs @@ -0,0 +1,80 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import { parseTleText } from '../../sources/tle.js'; +import { createIngestion } from './ingestion.js'; + +// The ISS set (1 January 2024) with its catalog field swapped per satellite. +const ISS_LINE1 = + '1 25544U 98067A 24001.50000000 .00016717 00000-0 30370-3 0 9993'; +const ISS_LINE2 = + '2 25544 51.6416 247.4627 0006703 130.5360 325.0288 15.50377579432323'; + +function tleSet(name, field) { + return [ + name, + ISS_LINE1.replace('25544', field), + ISS_LINE2.replace('25544', field), + ].join('\n'); +} + +function ingestionFor(catalogText) { + const state = { + _trackingRefreshEpoch: 0, + _activeUpdateControllers: new Set(), + _pointCollection: { + removeAll() {}, + add: (point) => point, + }, + _points: new Map(), + _orbitPaths: new Map(), + _catalog: new Map(), + _detectionObjects: new Map(), + _denseLoadToken: 0, + _catalogRevision: 0, + _params: { catalog: 'core', showOrbits: false }, + }; + const parts = { + orbits: { + parseTLE: parseTleText, + propagatePosition: () => ({ longitude: 0, latitude: 0, altitude: 4e5 }), + }, + labels: { _syncIssOverlay() {} }, + controls: { _pointStyleFor: () => ({}) }, + rendering: { _showOrbitPath() {} }, + tracking: { + async _reconcileTrackedSubjectContext() {}, + _applyPendingTrackingRestore() {}, + }, + catalog: {}, + }; + const source = { + async readGroup() { + return { ok: true, text: catalogText }; + }, + }; + const { methods } = createIngestion({ state, services: {}, parts, source }); + return { state, update: () => methods.update({ scene: {} }) }; +} + +test('every Alpha-5 satellite keeps its own catalog entry through dedupe', async (t) => { + t.mock.method(console, 'log', () => {}); + t.mock.method(console, 'warn', () => {}); + const { state, update } = ingestionFor( + [ + tleSet('FIRST', 'A0000'), + tleSet('SECOND', 'B0001'), + tleSet('THIRD', 'Z9999'), + ].join('\n'), + ); + await update(); + assert.deepEqual( + [...state._catalog].map(([id, entry]) => [id, entry.name]), + [ + [100000, 'FIRST'], + [110001, 'SECOND'], + [339999, 'THIRD'], + ], + ); + assert.deepEqual([...state._points.keys()], [100000, 110001, 339999]); + assert.equal(state._count, 3); +}); diff --git a/src/sources/tle.js b/src/sources/tle.js index f5fad48bd..f2b2ca3d7 100644 --- a/src/sources/tle.js +++ b/src/sources/tle.js @@ -21,50 +21,47 @@ export function parseTleText(text) { return result; } +// Alpha-5: a letter A–Z, skipping I and O, then exactly four digits. +const ALPHA5 = /^[A-HJ-NP-Z]\d{4}$/; +const DIGITS = /^\d+$/; + /** - * The catalog number an Alpha-5 field stands for, or null for any other field. + * The catalog number a catalog field stands for, or null for any other field. * - * Since the catalog passed 99999 (July 2026) a TLE writes the numbers above it - * in Alpha-5: a letter A–Z, skipping I and O, stands for 10–33 in the - * ten-thousands place, so 100000 is "A0000" and 339999 is "Z9999". + * A field is either plain digits or Alpha-5, the TLE spelling of numbers above + * 99999: the letter stands for 10–33 in the ten-thousands place, skipping I + * and O, so 100000 is "A0000" and 339999 is "Z9999". Anything else (a sign, a + * decimal point, hex, an embedded space, a lowercase or trailing letter) is + * not a catalog number. + * @param {string} field the field with any leading spaces already removed + * @returns {number | null} */ -function alpha5CatalogNumber(field) { +function decodeCatalogField(field) { + if (DIGITS.test(field)) return Number(field); + if (!ALPHA5.test(field)) return null; const letter = field[0]; - if ( - letter >= 'A' && - letter <= 'Z' && - letter !== 'I' && - letter !== 'O' && - /^\d{4}$/.test(field.slice(1)) - ) { - let tens = letter.charCodeAt(0) - 65 + 10; - if (letter > 'I') tens -= 1; - if (letter > 'O') tens -= 1; - return tens * 10000 + Number(field.slice(1)); - } - return null; + let tens = letter.charCodeAt(0) - 65 + 10; + if (letter > 'I') tens -= 1; + if (letter > 'O') tens -= 1; + return tens * 10000 + Number(field.slice(1)); } /** - * NORAD catalog number of a satrec as a number. + * NORAD catalog number of a satrec as a number, or NaN. * * satellite.js keeps the TLE's five-character field verbatim in * `satrec.satnum`, and `Number("A0000")` is NaN, so every Alpha-5 satellite - * would share one NaN key. An Alpha-5 field decodes to its number; a - * five-digit field is unchanged; anything else stays NaN. + * would share one NaN key. Digits of any length (an OMM satrec carries the + * number unpadded) and Alpha-5 decode; anything else, blank included, is NaN. * @param {string} satnum * @returns {number} */ export function noradIdFromSatnum(satnum) { - const field = String(satnum ?? '').trim(); - return alpha5CatalogNumber(field) ?? Number(field); + return decodeCatalogField(String(satnum ?? '').trim()) ?? Number.NaN; } -/** The NORAD catalog number from TLE line 1, or null. */ +/** The NORAD catalog number from TLE line 1 (columns 3–7), or null. */ export function tleCatalogNumber(line1) { const field = String(line1).slice(2, 7); - const alpha5 = alpha5CatalogNumber(field); - if (alpha5 !== null) return alpha5; - const number = Number.parseInt(field, 10); - return Number.isInteger(number) ? number : null; + return decodeCatalogField(field.replace(/^ +/, '')); } diff --git a/src/tools/queries/space.test.mjs b/src/tools/queries/space.test.mjs index 5ff267ce1..d0f2ffca8 100644 --- a/src/tools/queries/space.test.mjs +++ b/src/tools/queries/space.test.mjs @@ -72,14 +72,39 @@ test('Alpha-5 catalog numbers decode, skipping I and O', () => { } }); -test('every other catalog field reads as it did before', () => { +test('a digit catalog field reads as it did before', () => { assert.equal(noradIdFromSatnum('25544'), 25544); assert.equal(noradIdFromSatnum('00404'), 404); - assert.equal(noradIdFromSatnum(''), 0); // as Number('') did - for (const field of ['I0000', 'O0000', 'a0404', 'A00']) { - assert.ok(Number.isNaN(noradIdFromSatnum(field)), field); - assert.equal(tleCatalogNumber(`1 ${field}U`), null, field); + assert.equal(noradIdFromSatnum(' 404'), 404); + assert.equal(tleCatalogNumber('1 25544U'), 25544); + assert.equal(tleCatalogNumber('1 404U'), 404); + // An OMM satrec carries the number unpadded, past five digits. + assert.equal(noradIdFromSatnum('100000'), 100000); +}); + +test('a malformed catalog field is no number', () => { + for (const field of [ + 'I0000', + 'O0000', + 'a0404', + 'A00', + '2554A', + '255 4', + '-1234', + '0x1F0', + '1.5', + '1e3', + '', + ' ', + ]) { + assert.ok(Number.isNaN(noradIdFromSatnum(field)), JSON.stringify(field)); + assert.equal( + tleCatalogNumber(`1 ${field.padEnd(5)}U`), + null, + JSON.stringify(field), + ); } + assert.ok(Number.isNaN(noradIdFromSatnum(undefined))); }); test('a satellite numbered above 99999 keeps its number and is found by it', async () => { From 2f4b84600b94cea1c8387c8814c79da3c6b1193d Mon Sep 17 00:00:00 2001 From: Nurstar <127499386+NurstarK@users.noreply.github.com> Date: Sat, 17 Oct 2026 21:13:47 +0200 Subject: [PATCH 8/9] fix(deps): bump dompurify, sharp and source-map-js for disclosed advisories (#1094) Update DOMPurify 3.4.15 -> 3.4.17, Sharp 0.35.4 -> 0.35.5 (with its @img platform packages) and source-map-js 1.2.1 -> 1.2.2 in package-lock.json for GHSA-p98j-92pf-mc4p, GHSA-6688-9rhm-gjv2, GHSA-wq5f-xc86-pv6w and GHSA-68fv-2mgg-jv7q. npm audit reports no vulnerabilities. --- .../1094-dependency-security-patches.md | 4 + docs/CURRENT-STATE.md | 6 +- package-lock.json | 248 +++++++++--------- 3 files changed, 131 insertions(+), 127 deletions(-) create mode 100644 changelog.d/1094-dependency-security-patches.md diff --git a/changelog.d/1094-dependency-security-patches.md b/changelog.d/1094-dependency-security-patches.md new file mode 100644 index 000000000..058359efc --- /dev/null +++ b/changelog.d/1094-dependency-security-patches.md @@ -0,0 +1,4 @@ +- Update DOMPurify to 3.4.17, Sharp to 0.35.5 and source-map-js to 1.2.2 for + GHSA-p98j-92pf-mc4p, GHSA-6688-9rhm-gjv2, GHSA-wq5f-xc86-pv6w and + GHSA-68fv-2mgg-jv7q; `npm audit` now reports no vulnerabilities (NurstarK, + #1094). diff --git a/docs/CURRENT-STATE.md b/docs/CURRENT-STATE.md index 105fdd37f..b23f2f011 100644 --- a/docs/CURRENT-STATE.md +++ b/docs/CURRENT-STATE.md @@ -5459,9 +5459,9 @@ When runtime behavior or architecture changes, update this file in the same chan ## Dependency security baseline -The lockfile uses DOMPurify 3.4.15, protobufjs 8.8.0, PostCSS 8.5.28, and -nanoid 3.3.19. Cesium remains on 1.138.0. Browser QA uses Puppeteer 25.10.0; -image-processing tools use Sharp 0.35.4. QA scripts await Puppeteer's asynchronous +The lockfile uses DOMPurify 3.4.17, protobufjs 8.8.0, PostCSS 8.5.28, +source-map-js 1.2.2 and nanoid 3.3.19. Cesium remains on 1.138.0. Browser QA +uses Puppeteer 25.10.0; image-processing tools use Sharp 0.35.5. QA scripts await Puppeteer's asynchronous executable-path lookup before testing or passing the path to Chrome. Supported Node versions remain 24.14.x and 26.x. Use `npm ci` to reproduce the checked-in dependency tree. diff --git a/package-lock.json b/package-lock.json index e89fd9c65..7b6d3476f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -548,9 +548,9 @@ } }, "node_modules/@img/sharp-darwin-arm64": { - "version": "0.35.4", - "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.35.4.tgz", - "integrity": "sha512-Uhfl4V4lhP2nbUVF9+hyH1+luj86f1gUFeo8ALYxFoULoU+G87D43BfeMP8XHsk9boxAnCY/bf2EHwhA7MuGsA==", + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.35.5.tgz", + "integrity": "sha512-QRUlFQ0WxvdWyqqG/WtI3iupfD5rBzmCHXSdPsY91sAtVtTo7Q4cb6zOccZ3gqEqkr0f1As1ehLqmEpDsRf+lg==", "cpu": [ "arm64" ], @@ -567,13 +567,13 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-darwin-arm64": "1.3.3" + "@img/sharp-libvips-darwin-arm64": "1.3.4" } }, "node_modules/@img/sharp-darwin-x64": { - "version": "0.35.4", - "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.35.4.tgz", - "integrity": "sha512-hWniXY3bG5qKpkKrAwPe4y+VTPmf086YQAnkxWh7uA1YrlRouWGa0M0Mxj3ZjnXFkv7/TD1bTy9lGUK26vRvWw==", + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.35.5.tgz", + "integrity": "sha512-+BR255RhDlpygUpOc/Jdt1nT6DQ3XG/ERo5wbcdOf5Q320dKtPCKPLR1LJs9VGXRaMa8l1uUa0tkCNOXiAxZUw==", "cpu": [ "x64" ], @@ -590,13 +590,13 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-darwin-x64": "1.3.3" + "@img/sharp-libvips-darwin-x64": "1.3.4" } }, "node_modules/@img/sharp-freebsd-wasm32": { - "version": "0.35.4", - "resolved": "https://registry.npmjs.org/@img/sharp-freebsd-wasm32/-/sharp-freebsd-wasm32-0.35.4.tgz", - "integrity": "sha512-lIsKw/BU+kjB4eZjxrYrZmwOJYi3Ajrv66iAlBmUPyKc3HpnloevB1g3wxGD9P/5BbQ1brBGl65VRRrCvQDEqA==", + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-freebsd-wasm32/-/sharp-freebsd-wasm32-0.35.5.tgz", + "integrity": "sha512-Y/z91nEZ4uIBX5X3nfTovjU9lHNKFYbL2lpHCLVNmXQK03VIZvXBBt0KxbPGp2SdGSF+2mQU4e+hQaWOt86iAw==", "dev": true, "license": "Apache-2.0", "optional": true, @@ -604,7 +604,7 @@ "freebsd" ], "dependencies": { - "@img/sharp-wasm32": "0.35.4" + "@img/sharp-wasm32": "0.35.5" }, "engines": { "node": ">=20.9.0" @@ -614,9 +614,9 @@ } }, "node_modules/@img/sharp-libvips-darwin-arm64": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.3.3.tgz", - "integrity": "sha512-suTBPTDGrI9WodccaDdwZItTSaBYASlBk1NSfElSHrUfzu3szG6lvIF58+WiFvnfzuK8ZBFS5zE00PxqxnRiPg==", + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.3.4.tgz", + "integrity": "sha512-5R89nBYiRdUlSWJxPhO+GVtaXzXSxKnRu/xqMn3KTA3L9EB9Oy/P+Nn2f2vlhPuUdy/Zusb2DarbyTpGCfEDuw==", "cpu": [ "arm64" ], @@ -631,9 +631,9 @@ } }, "node_modules/@img/sharp-libvips-darwin-x64": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.3.3.tgz", - "integrity": "sha512-FVJZ5mITMobmXIz/hPDTw0EintTW5H3WfrxwLqEqjiIihlu+hVRyGrFQ60xl0Lxn7Bt3zdpevPaQi0HEzqz9fw==", + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.3.4.tgz", + "integrity": "sha512-iR2OKH80yi0U+dUplyh3/xdpFvps6YkCwsXenIJxqxR1v9o+xtKTGbS9H7cps+2Vxjc8B1j96p75NmTGjIhtpQ==", "cpu": [ "x64" ], @@ -648,9 +648,9 @@ } }, "node_modules/@img/sharp-libvips-linux-arm": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.3.3.tgz", - "integrity": "sha512-3rbU4vqXXc3hY/OiXdl52xZvT0F1yEngWfvqudtPJg/KkyiaQw2DRsFrNzpmLvfavbwOq3qXn36GP8obHRULQA==", + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.3.4.tgz", + "integrity": "sha512-LmRtTsOHuvM2+wlO2Db37dx5MiZhB0FvSunciw48YjdOkZz9KAiRbm8ujeMOA1INqmei5NapFxYEK1D1ZSidmw==", "cpu": [ "arm" ], @@ -665,9 +665,9 @@ } }, "node_modules/@img/sharp-libvips-linux-arm64": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.3.3.tgz", - "integrity": "sha512-0DaL0A6Xu6sQSQFwe4iVCrKWU2cCTItnRsYsCdxAMm9NF6twAA9BKnoqy4hqz4+azQ0JHuA26qiUKsf1XJ/v5A==", + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.3.4.tgz", + "integrity": "sha512-Y3dgX/6lE2QhQb+Gxy0WZxfg9MEm/JBjamZpS2IklP7xIQoKN4hzAm7KcMVGtaVDt3neE9OKBC7vAfonA/Lr1A==", "cpu": [ "arm64" ], @@ -682,9 +682,9 @@ } }, "node_modules/@img/sharp-libvips-linux-ppc64": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.3.3.tgz", - "integrity": "sha512-cdn1OvUBwsXhbC0zSzJnNzf5MZ/mTrobawDvNXBTxe8VtqKAm0sRuEY2Evzovb/w9JMk4TvRxqt1mekSuJz64w==", + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.3.4.tgz", + "integrity": "sha512-Le6boB8Tai0Nis+gIxIpKx68UDVVIqdR8Tin5Yf1z2LJJQLDJvCDRqRu+jC2qCoD+eIomonmOwB4smBRxfVpYQ==", "cpu": [ "ppc64" ], @@ -699,9 +699,9 @@ } }, "node_modules/@img/sharp-libvips-linux-riscv64": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.3.3.tgz", - "integrity": "sha512-HjPVx7yKz+0lqdhDlTw1tt90wamBoxhiXpvl1XZpJLiHH4RCJ5yDTqH+VlYPv2fwFs89JFw4c1IexYOcQUi4IQ==", + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.3.4.tgz", + "integrity": "sha512-aHkkIEHPRdQEegJN20MLmGtxYD9R2wQr3Cwpddnu5+YKMt6Uzax7S9h5gpZTo8wyrGuZSlfQ63OevL5mTyOC7Q==", "cpu": [ "riscv64" ], @@ -716,9 +716,9 @@ } }, "node_modules/@img/sharp-libvips-linux-s390x": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.3.3.tgz", - "integrity": "sha512-neWLh+3yCNThxnfy3c4BbVBeGgt9aftno+XbT56iK28RgeDs3UOFWviLWlUu0bArYVYJaFDK+RRohbicUNCm8Q==", + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.3.4.tgz", + "integrity": "sha512-ra/mB6MikESDUO7Yg+Mi95bFBb9GsObURuhnOv3OqknjGe9sZrG8tCe9q0xSIGrtLgvgw0gKnFWcK4blSgQOuQ==", "cpu": [ "s390x" ], @@ -733,9 +733,9 @@ } }, "node_modules/@img/sharp-libvips-linux-x64": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.3.3.tgz", - "integrity": "sha512-4vKmvAst9nrowcqquKFAyZJUDolUaIp8uRiN0mWFguJ1IplC9/pitXtlnnlU4aa/eJw3J7i67V+pwUL+wZGdsA==", + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.3.4.tgz", + "integrity": "sha512-GJ//SSXbnwSDes02umB3nDJLFcQzw8a18V8fyhqr6tV515tOEMdImjjxj1AoafMRz56F3PHgftnj1QEKSU1zkw==", "cpu": [ "x64" ], @@ -750,9 +750,9 @@ } }, "node_modules/@img/sharp-libvips-linuxmusl-arm64": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.3.3.tgz", - "integrity": "sha512-Y9kQaLMuNoB0bPYOOdcZMaseNrFpPodIWWMrx+CZyydf2xn68j9WYc6sWWRrDwNkzCQjKYfc68L7jKjGlHMibw==", + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.3.4.tgz", + "integrity": "sha512-hvulFwtjUcagsis6BBxHwGFwWoNZjgYmULGVrZcyfNbjA8hKILbRxGg15/7w5HDyXHXUos/j6baAWqnCyQ2DWA==", "cpu": [ "arm64" ], @@ -767,9 +767,9 @@ } }, "node_modules/@img/sharp-libvips-linuxmusl-x64": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.3.3.tgz", - "integrity": "sha512-fj8Mv0HHfD1Rr+4I68+3agJynxDWtBFgicTbSOb9Bke6pIwzGcJ+RX/yHjmiEGFMCavY/dxvem7MyNaJF+wDiw==", + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.3.4.tgz", + "integrity": "sha512-6zXKeE/p39I1AmA3cJG35eyBGNqNddLnUXjhwBnsGjFPWqf5VKkDBEqaEkPDoTEtkxwi2vv8Tcr2mDyP4So7Fg==", "cpu": [ "x64" ], @@ -784,9 +784,9 @@ } }, "node_modules/@img/sharp-linux-arm": { - "version": "0.35.4", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.35.4.tgz", - "integrity": "sha512-7OAS8gI0EReKGVN2HssHlM6umJgxF5VI3xN0p9FA91p/YO+ou5hiNghLdZ5BEHztwaaK5+bLKRf8x/o2L2nk9A==", + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.35.5.tgz", + "integrity": "sha512-LEaXK2WdXVK5ykcw0buWyPMsmLLL2vpHLD6yrNSW+JGEL3BZPA4tpKN6iaMc4AxTTAoaX/sU1rOL51lcIz48ZQ==", "cpu": [ "arm" ], @@ -803,13 +803,13 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-arm": "1.3.3" + "@img/sharp-libvips-linux-arm": "1.3.4" } }, "node_modules/@img/sharp-linux-arm64": { - "version": "0.35.4", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.35.4.tgz", - "integrity": "sha512-De4jpEnAU8Hd5oT0j1G3uL4ZvTuipVMn7YC6vPaJhy6/7EwEae0SVAoBrUMYQbkLGDm85taVWwuPc1a44LTzCQ==", + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.35.5.tgz", + "integrity": "sha512-LYVx5JTsOM2CBzmxreh+nl64/3H6Xb09iSLknqH47z2T2DFFxDeFLP5y4dJwe6H7uGQlHPyEEtIqyo3DYsRwdQ==", "cpu": [ "arm64" ], @@ -826,13 +826,13 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-arm64": "1.3.3" + "@img/sharp-libvips-linux-arm64": "1.3.4" } }, "node_modules/@img/sharp-linux-ppc64": { - "version": "0.35.4", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.35.4.tgz", - "integrity": "sha512-2oYZJeIl4kCcMGk4ouZVjnkCtFrpQFlNEtJ6GbxzhHQchwH0NH/qEb9ykmOl29dqwMq+JhFdZn+1ak2FKhI9fQ==", + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.35.5.tgz", + "integrity": "sha512-QVxAAq8evVRI9ia2vqgwrmWucn5Dfv+JdWzj75pD8omHLPSP7f8p20O8jxzjCcuCEQEOtYOZUmX1hkiZ0kdevA==", "cpu": [ "ppc64" ], @@ -849,13 +849,13 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-ppc64": "1.3.3" + "@img/sharp-libvips-linux-ppc64": "1.3.4" } }, "node_modules/@img/sharp-linux-riscv64": { - "version": "0.35.4", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.35.4.tgz", - "integrity": "sha512-cPbNChoRURAWdebDIHSenxRpgEdy7JkPydSnUxRm9VvKD7m0/xVaR/8Fzlu81pk5nHEvHH87UZUA7cTtwnbJSA==", + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.35.5.tgz", + "integrity": "sha512-LtdreXguaavKODPIfzJ4kffx7UNt1omwtK0rch4EBbbSTXPnxWmYSayXdLJw0fJzQ97kHt1gL/yh4tvU+nCyRQ==", "cpu": [ "riscv64" ], @@ -872,13 +872,13 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-riscv64": "1.3.3" + "@img/sharp-libvips-linux-riscv64": "1.3.4" } }, "node_modules/@img/sharp-linux-s390x": { - "version": "0.35.4", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.35.4.tgz", - "integrity": "sha512-RY0JFY8Fd6RonCBtHz+DvadaPkXDSI1AUn6yWL9TipqkZ1vY8w8evqdgyDFnkm4/K1ve1TvZiaePP5oSd4+WVQ==", + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.35.5.tgz", + "integrity": "sha512-UZasTOFiYzotTsGOCu42BfUzP6Tu6Do/947iRm1RsLKvlllxwGcn4RN27LibGWceix4Y+Pmw3jsnTcCQIgWjqA==", "cpu": [ "s390x" ], @@ -895,13 +895,13 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-s390x": "1.3.3" + "@img/sharp-libvips-linux-s390x": "1.3.4" } }, "node_modules/@img/sharp-linux-x64": { - "version": "0.35.4", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.35.4.tgz", - "integrity": "sha512-9qvvEAuk8k89TfWUoX2htWjbAMX8p+NxCppjpcg5k6xMsjhBQPTsoIh36h9Qde4WRuGpJeYnOjdosDn/cnv+OA==", + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.35.5.tgz", + "integrity": "sha512-SxFtLTeJInhAA9Q836kux2vZNeOBQEx658qvbboZScr0wIARym3IcGmW7KpVD5sbVg0Ojy+udFQdayYIZyoNog==", "cpu": [ "x64" ], @@ -918,13 +918,13 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-x64": "1.3.3" + "@img/sharp-libvips-linux-x64": "1.3.4" } }, "node_modules/@img/sharp-linuxmusl-arm64": { - "version": "0.35.4", - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.35.4.tgz", - "integrity": "sha512-KB5jxpfWQTr0nc3xdHtWChdbifHrBGsd2SM62Eyxrl8afikm+f5qGBU75SJIZBT/S1MC8XyacdlXBMSWq6OURA==", + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.35.5.tgz", + "integrity": "sha512-9HbMclmI1zlNkFRs3z9/eBtDjfD0sGlrX1z6b1qwmiFY5ElDLh4BC0LPBdVp7z1DXFiKlIcznf+ZlsuZzLxQqg==", "cpu": [ "arm64" ], @@ -941,13 +941,13 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linuxmusl-arm64": "1.3.3" + "@img/sharp-libvips-linuxmusl-arm64": "1.3.4" } }, "node_modules/@img/sharp-linuxmusl-x64": { - "version": "0.35.4", - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.35.4.tgz", - "integrity": "sha512-f+eZJZIQNEEd26RPSW+76chwOf1XtA2Y/O+5ocVyLliHkeih3e+jhLVBdNTd2rS3IbNXK8+ug93Vf5ZXtF5Lxg==", + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.35.5.tgz", + "integrity": "sha512-4KOphqB035HrVdqLZfCgMzzERrQkkzOwRhl4OAkRO1YCldbaFjySXMaK534Mo0V+LndnlJk+sbUyLeU0ULyD1A==", "cpu": [ "x64" ], @@ -964,13 +964,13 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linuxmusl-x64": "1.3.3" + "@img/sharp-libvips-linuxmusl-x64": "1.3.4" } }, "node_modules/@img/sharp-wasm32": { - "version": "0.35.4", - "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.35.4.tgz", - "integrity": "sha512-zQnl4Kwp7Q6NHsENtU2T/00Zi+w3AQNwz3+UaTyVBy2FpXrzXzGjndpK61onhZjRtRpQXxCTeqw19bVyXOh7jA==", + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.35.5.tgz", + "integrity": "sha512-Ptsga1su4tQx+LLF1ECS9U6nz5kmrXKo6XVbtR48Ke3ZRxxgaWBu7IDtEe1quo8hiupwm6WFqxVlXaSf7IINGQ==", "dev": true, "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", "optional": true, @@ -985,9 +985,9 @@ } }, "node_modules/@img/sharp-webcontainers-wasm32": { - "version": "0.35.4", - "resolved": "https://registry.npmjs.org/@img/sharp-webcontainers-wasm32/-/sharp-webcontainers-wasm32-0.35.4.tgz", - "integrity": "sha512-ESfNkywmCfPNyaZjxooddJQiQ+l/nTpGEOGthxiLnIHXC/CmcBixnfwUleX9mCz9ovrUUvKMap/pm8RYbzfwaA==", + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-webcontainers-wasm32/-/sharp-webcontainers-wasm32-0.35.5.tgz", + "integrity": "sha512-hfhF/FmoQyTUkA0bIKFOtw536BQSeBMe6BF6QyWlrPxT754+TFLaZ7sKKTfvvM0yJgKgaYTwnFCIZ/GuDw5SUA==", "cpu": [ "wasm32" ], @@ -995,7 +995,7 @@ "license": "Apache-2.0", "optional": true, "dependencies": { - "@img/sharp-wasm32": "0.35.4" + "@img/sharp-wasm32": "0.35.5" }, "engines": { "node": ">=20.9.0" @@ -1005,9 +1005,9 @@ } }, "node_modules/@img/sharp-win32-arm64": { - "version": "0.35.4", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.35.4.tgz", - "integrity": "sha512-iNdlBX9gLVvqe2I3uIJSIKTq6wckP/DYxZtcqxm09x5Gi24DnFBmPAWZmr60ZyYMG0xlzo6goG3670ar+RXvRw==", + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.35.5.tgz", + "integrity": "sha512-X4t7g+7ZA5DKblCBEXGjUqqemj4vczING/5viFwAL8h4N3qYeyjwdCvRLHi4EdOUI+2Z7UFlp1VM+p/AuEtm6Q==", "cpu": [ "arm64" ], @@ -1025,9 +1025,9 @@ } }, "node_modules/@img/sharp-win32-ia32": { - "version": "0.35.4", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.35.4.tgz", - "integrity": "sha512-kqRsbaa5CS6KHlpxnN7WhE6vAAugXyZButpRdvDWetlv6Qv4N9WTcrWzF7tXfB9T7MsoadqdI8hmwLq6UlLvtw==", + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.35.5.tgz", + "integrity": "sha512-5Zm82LoBc43nhwNybZlG7Y1KO//Zhsn306fQl29ZOuStHLGTo3BWL83q3cznX0poxSAMuYL1On/BHBxkBeKr6A==", "cpu": [ "ia32" ], @@ -1045,9 +1045,9 @@ } }, "node_modules/@img/sharp-win32-x64": { - "version": "0.35.4", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.35.4.tgz", - "integrity": "sha512-XtmnYhBcrORsJ4XJngyzr/EWP0hRZLAZRFaApdKuviyqF78+ylxh2y06ZmtULAMOnObJ3ucpN0AcwSWnMowTRg==", + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.35.5.tgz", + "integrity": "sha512-x76eH0vEiHlcMQu8Y8IenntaACtddpT6W0wmXtWrnKcnKI7ME5DdgqhAD6SEWOEl1v2zDvkZDhFA9KnURwpfqg==", "cpu": [ "x64" ], @@ -1833,9 +1833,9 @@ "integrity": "sha512-6QvTW9mrGeIegrFXdtQi9pk7O/nSK6lSdXW2eqUspN5LWD7UTji2Fqw5V2YLjBpHEoU9Xl/eUWNpDeZvoyOv2w==" }, "node_modules/dompurify": { - "version": "3.4.15", - "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.15.tgz", - "integrity": "sha512-EUBjM+B+lkDE41iE82DDSCfkoPGfXx8IxFxPMjNzm/Uk4xDet77rTN9wqlxlVg71kK7XGuUMv6wUxJUwwv+Xyw==", + "version": "3.4.17", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.17.tgz", + "integrity": "sha512-B/pSlYVHQ58kqISD/yDjuJ1yfzI34m1gF4lAod2bwSRhiN4K5Cl3H8Af7hqB2PmTL911tdEudnnXn5FnhRoz0w==", "license": "(MPL-2.0 OR Apache-2.0)", "optionalDependencies": { "@types/trusted-types": "^2.0.7" @@ -2782,9 +2782,9 @@ "license": "ISC" }, "node_modules/sharp": { - "version": "0.35.4", - "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.35.4.tgz", - "integrity": "sha512-n++8XWcj+jCOr2IOl7h8LbKnGBDY4aPbmprMONBNFdn0ImXqpGVv5zliDs0V9HbmbCQLpbuo2ej9rAoOQTvMDA==", + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.35.5.tgz", + "integrity": "sha512-Ywn4OnzGukp7CDMrp08RQ50YKmuwG47brZgIVPTvBaaAfQlRlygrRqSrxdCiL9M+LlzLBiJ68IR1QqvzHyjC7g==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -2799,31 +2799,31 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-darwin-arm64": "0.35.4", - "@img/sharp-darwin-x64": "0.35.4", - "@img/sharp-freebsd-wasm32": "0.35.4", - "@img/sharp-libvips-darwin-arm64": "1.3.3", - "@img/sharp-libvips-darwin-x64": "1.3.3", - "@img/sharp-libvips-linux-arm": "1.3.3", - "@img/sharp-libvips-linux-arm64": "1.3.3", - "@img/sharp-libvips-linux-ppc64": "1.3.3", - "@img/sharp-libvips-linux-riscv64": "1.3.3", - "@img/sharp-libvips-linux-s390x": "1.3.3", - "@img/sharp-libvips-linux-x64": "1.3.3", - "@img/sharp-libvips-linuxmusl-arm64": "1.3.3", - "@img/sharp-libvips-linuxmusl-x64": "1.3.3", - "@img/sharp-linux-arm": "0.35.4", - "@img/sharp-linux-arm64": "0.35.4", - "@img/sharp-linux-ppc64": "0.35.4", - "@img/sharp-linux-riscv64": "0.35.4", - "@img/sharp-linux-s390x": "0.35.4", - "@img/sharp-linux-x64": "0.35.4", - "@img/sharp-linuxmusl-arm64": "0.35.4", - "@img/sharp-linuxmusl-x64": "0.35.4", - "@img/sharp-webcontainers-wasm32": "0.35.4", - "@img/sharp-win32-arm64": "0.35.4", - "@img/sharp-win32-ia32": "0.35.4", - "@img/sharp-win32-x64": "0.35.4" + "@img/sharp-darwin-arm64": "0.35.5", + "@img/sharp-darwin-x64": "0.35.5", + "@img/sharp-freebsd-wasm32": "0.35.5", + "@img/sharp-libvips-darwin-arm64": "1.3.4", + "@img/sharp-libvips-darwin-x64": "1.3.4", + "@img/sharp-libvips-linux-arm": "1.3.4", + "@img/sharp-libvips-linux-arm64": "1.3.4", + "@img/sharp-libvips-linux-ppc64": "1.3.4", + "@img/sharp-libvips-linux-riscv64": "1.3.4", + "@img/sharp-libvips-linux-s390x": "1.3.4", + "@img/sharp-libvips-linux-x64": "1.3.4", + "@img/sharp-libvips-linuxmusl-arm64": "1.3.4", + "@img/sharp-libvips-linuxmusl-x64": "1.3.4", + "@img/sharp-linux-arm": "0.35.5", + "@img/sharp-linux-arm64": "0.35.5", + "@img/sharp-linux-ppc64": "0.35.5", + "@img/sharp-linux-riscv64": "0.35.5", + "@img/sharp-linux-s390x": "0.35.5", + "@img/sharp-linux-x64": "0.35.5", + "@img/sharp-linuxmusl-arm64": "0.35.5", + "@img/sharp-linuxmusl-x64": "0.35.5", + "@img/sharp-webcontainers-wasm32": "0.35.5", + "@img/sharp-win32-arm64": "0.35.5", + "@img/sharp-win32-ia32": "0.35.5", + "@img/sharp-win32-x64": "0.35.5" }, "peerDependenciesMeta": { "@types/node": { @@ -2832,9 +2832,9 @@ } }, "node_modules/source-map-js": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", - "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.2.tgz", + "integrity": "sha512-KGj/8Y43x35aZVDtt+J4mK1hoLGHULMYfSkODJNQjNDC3oW1PqPoxMwo0pLUsWM/UEGzON/NxeHywEfNXNP3Vw==", "dev": true, "license": "BSD-3-Clause", "engines": { From 743ed639e56a037af10385eec01571d966f31205 Mon Sep 17 00:00:00 2001 From: monwiss <547445+monwiss@users.noreply.github.com> Date: Sat, 17 Oct 2026 15:18:32 -0400 Subject: [PATCH 9/9] fix(dev): stop the dev server watching .gev-cache and .gev-logs (#761) Providers write cache files and logs into .gev-cache and .gev-logs while the app runs. A serve-only standalone plugin adds both to server.watch.ignored, so the dev watcher no longer stats them; source changes still reload. --- changelog.d/761-dev-unwatched-runtime-dirs.md | 5 ++++ server/standalone/unwatched-cache.js | 28 +++++++++++++++++++ server/standalone/vite.config.js | 2 ++ src/tooling/viteBuild.test.mjs | 25 ++++++++++++++--- 4 files changed, 56 insertions(+), 4 deletions(-) create mode 100644 changelog.d/761-dev-unwatched-runtime-dirs.md create mode 100644 server/standalone/unwatched-cache.js diff --git a/changelog.d/761-dev-unwatched-runtime-dirs.md b/changelog.d/761-dev-unwatched-runtime-dirs.md new file mode 100644 index 000000000..d3aaf0a91 --- /dev/null +++ b/changelog.d/761-dev-unwatched-runtime-dirs.md @@ -0,0 +1,5 @@ +- The dev server no longer watches `.gev-cache` and `.gev-logs`. Providers write + cache files and logs there while the app runs, and none of it is source, so + the watcher now skips it; this guards against the DNS-lookup delays heavy + cache writes have caused, though no difference was measured on current main + (monwiss, #761). diff --git a/server/standalone/unwatched-cache.js b/server/standalone/unwatched-cache.js new file mode 100644 index 000000000..130d0ece4 --- /dev/null +++ b/server/standalone/unwatched-cache.js @@ -0,0 +1,28 @@ +/** + * Directories the app writes to while it runs. None is source, and none + * should reload anything. + */ +const RUNTIME_WRITE_GLOBS = Object.freeze([ + '**/.gev-cache/**', + '**/.gev-logs/**', +]); + +/** + * Keep the directories the app writes to out of the dev server's file watcher. + * + * Disk-cached providers write a file per response into `.gev-cache`, and the + * voice debug log appends to `.gev-logs`. Left watched, each write costs the + * watcher stat calls on libuv's thread pool, which `getaddrinfo` shares; heavy + * cache writes have been seen to delay upstream DNS lookups. + * + * @returns {import('vite').Plugin} Config-only plugin; no server hooks. + */ +export function unwatchedRuntimeDirsPlugin() { + return { + name: 'gev-unwatched-runtime-dirs', + apply: 'serve', + config: () => ({ + server: { watch: { ignored: [...RUNTIME_WRITE_GLOBS] } }, + }), + }; +} diff --git a/server/standalone/vite.config.js b/server/standalone/vite.config.js index a14292597..03a935092 100644 --- a/server/standalone/vite.config.js +++ b/server/standalone/vite.config.js @@ -6,6 +6,7 @@ import { localProviderPlugins } from '../providers/local.js'; import { localMcpPlugin } from '../mcp/plugin.js'; import { apiNotFoundPlugin } from './api-not-found.js'; import { standaloneVoiceTools } from './voiceTools.js'; +import { unwatchedRuntimeDirsPlugin } from './unwatched-cache.js'; const root = fileURLToPath(new URL('../../', import.meta.url)); @@ -20,6 +21,7 @@ export default defineConfig(({ command, mode }) => { ...localProviderPlugins({ realtime: { tools: standaloneVoiceTools() } }), localMcpPlugin(), apiNotFoundPlugin(), + unwatchedRuntimeDirsPlugin(), ], googleApiKey: process.env.GOOGLE_MAPS_API_KEY, cesiumToken: process.env.CESIUM_ION_TOKEN, diff --git a/src/tooling/viteBuild.test.mjs b/src/tooling/viteBuild.test.mjs index 86a9afb63..9e38a0295 100644 --- a/src/tooling/viteBuild.test.mjs +++ b/src/tooling/viteBuild.test.mjs @@ -89,17 +89,34 @@ test('root config retains existing named exports and standalone provider order', assert.equal(compatibility[name], value, name); const config = standaloneConfig({ mode: 'test' }); assert.deepEqual( - config.plugins.slice(3, -4).map((plugin) => plugin.name), + config.plugins.slice(3, -5).map((plugin) => plugin.name), providers.localProviderPlugins().map((plugin) => plugin.name), ); - assert.equal(config.plugins.at(-5).name, 'gev-key-setup'); + assert.equal(config.plugins.at(-6).name, 'gev-key-setup'); // The local MCP route follows every provider and precedes the API fallback. - assert.equal(config.plugins.at(-4).name, 'local-mcp'); - assert.equal(config.plugins.at(-3).name, 'api-not-found'); + assert.equal(config.plugins.at(-5).name, 'local-mcp'); + assert.equal(config.plugins.at(-4).name, 'api-not-found'); + // Config-only, so it sits after the provider slice rather than in it. + assert.equal(config.plugins.at(-3).name, 'gev-unwatched-runtime-dirs'); assert.equal(config.plugins.at(-2).name, 'embed-framing'); assert.equal(config.plugins.at(-1).name, 'panel-build'); }); +test('directories the server writes while it runs are kept out of the dev watcher', () => { + // Providers write cache files and logs here while the app runs; none of it is + // source, so the dev watcher has no reason to stat it. + const config = standaloneConfig({ mode: 'test' }); + const plugin = config.plugins.find( + ({ name }) => name === 'gev-unwatched-runtime-dirs', + ); + // Dev only: preview and build have no watcher. + assert.equal(plugin.apply, 'serve'); + assert.deepEqual(plugin.config().server.watch.ignored, [ + '**/.gev-cache/**', + '**/.gev-logs/**', + ]); +}); + test('build export resolves in Node and has no browser fallback', async () => { const exported = await import('gods-eye-view/build/vite'); assert.equal(exported.createBrowserViteConfig, createBrowserViteConfig);