Merge branch 'main' into feat/voice-geometry

Picks up #1308 (Overpass admission), #114 (Sea Marks and Ski Pistes),
#1206 (radio country coverage) and #1435. Resolutions:
- SECURITY.md: main's rewritten cross-site paragraph, with voice place
  search listed beside /api/overpass among the keyless map routes.
- scripts/format-scope.json: both sides. A merge on main dropped 97 entries
  (being restored there); keeping them, this PR's 12 and main's 2 new ones
  keeps every file in format:check. All 1,222 paths exist.
- Translation catalogs: main's two new keys and this PR's 47, no overlap.
- Layer count 36 (33 + OSM Places + Sea Marks + Ski Pistes); both schema
  pins re-derived, the first-run length is exactly 30,919 + 292 + 107.
This commit is contained in:
Manjunath committed 2026-11-03 22:16:52 +05:30
commit 1e12d5ba26
56 files changed
+1834 -209

No files matched your search

+85 -13
View File
@@ -1,11 +1,27 @@
/** A reveal must beat the one-second idle rotation refresh, regardless of FPS. */
/**
* Wall-clock budget for EACH observed frame, regardless of FPS. Both the settle
* frame (measured from the forced moveEnd) and the reveal frame (measured from
* the settle frame that hid the contact) must land inside it, so neither can be
* repaired by the one-second idle rotation refresh before it is read. The
* refresh clock starts at the fleet tick (preRender) of the frame the budget is
* measured from, so the budget must leave room for that frame's own render
* time: half the refresh interval tolerates frames up to ~500 ms, and a ~300 ms
* software-rendered frame (headless SwiftShader) fits with margin.
*/
export const ARRIVAL_OBSERVATION_MS = 500;
/** A measured rotation error at or above this fails the arrival check. */
export const ARRIVAL_MAX_ERROR_DEG = 10;
/** Attempts allowed when every earlier one timed out without a wrong angle. */
export const ARRIVAL_MAX_ATTEMPTS = 2;
/**
* Observe the first completed moveEnd frame and the first subsequent reveal.
* The deadline starts when moveEnd is forced, before its rotation baseline, so
* waiting for either frame cannot let the one-second idle refresh mask a bug.
* `read` snapshots visibility and rotation error against the current position.
* The settle deadline starts when moveEnd is forced, before its rotation
* baseline; the reveal deadline restarts at the settle frame, which is where
* the contact is hidden. `read` snapshots visibility and rotation error
* against the current position.
*/
export function observeArrivalRotation({
scene,
@@ -18,6 +34,7 @@ export function observeArrivalRotation({
}) {
return new Promise((resolve, reject) => {
const startedAt = now();
let phaseStartedAt = startedAt;
let moveEnd = null;
let moveEndMs = null;
let revealFrames = 0;
@@ -34,24 +51,42 @@ export function observeArrivalRotation({
cleanup();
resolve(result);
};
const expired = () => ({
// A timeout keeps every angle it measured: the settle angle before a
// reveal timeout, and the angle on a late frame itself (a late frame is
// read first; the idle refresh can only repair an angle, so a wrong one
// seen late was wrong on time too). `arrivalAngleViolated` then makes the
// timeout final instead of retryable.
const expired = (late = null) => ({
error: `arrival ${moveEnd ? 'reveal' : 'moveEnd'} observation exceeded ${ARRIVAL_OBSERVATION_MS} ms`,
elapsedMs: now() - startedAt,
moveEndMs,
...(moveEnd
? { dMoveEnd: moveEnd.rotationError }
: late
? { dMoveEnd: late.rotationError }
: {}),
...(moveEnd && late?.shown ? { dReveal: late.rotationError } : {}),
flipFrame: -1,
});
const armDeadline = () => {
clearTimer(timer);
timer = setTimer(() => finish(expired()), ARRIVAL_OBSERVATION_MS);
};
remove = scene.postRender.addEventListener(() => {
if (done) return;
try {
const elapsedMs = now() - startedAt;
if (elapsedMs >= ARRIVAL_OBSERVATION_MS) {
finish(expired());
const at = now();
const elapsedMs = at - startedAt;
const sample = read();
if (at - phaseStartedAt >= ARRIVAL_OBSERVATION_MS) {
finish(expired(sample));
return;
}
const sample = read();
if (moveEnd === null) {
moveEnd = sample;
moveEndMs = elapsedMs;
phaseStartedAt = at;
armDeadline();
startReveal();
} else {
revealFrames++;
@@ -76,7 +111,7 @@ export function observeArrivalRotation({
reject(error);
}
});
timer = setTimer(() => finish(expired()), ARRIVAL_OBSERVATION_MS);
armDeadline();
try {
startMoveEnd();
scene.requestRender();
@@ -87,14 +122,51 @@ export function observeArrivalRotation({
});
}
/**
* Whether an observation measured a rotation error over the threshold on any
* frame it captured, including the settle frame of a reveal that then timed
* out. Such an observation is a demonstrated failure, never inconclusive.
*/
export function arrivalAngleViolated(observation) {
const limit = (ARRIVAL_MAX_ERROR_DEG * Math.PI) / 180;
return [observation?.dMoveEnd, observation?.dReveal].some(
(error) => Number.isFinite(error) && error >= limit,
);
}
/**
* Observe arrival rotation, retrying only an inconclusive timeout. A wall-clock
* timeout means the frame landed where the idle refresh could already have
* repaired it, so it earns another attempt; an observation that measured a
* wrong angle (or succeeded) is final, so a later clean attempt can never
* replace it. `prepare` runs before every attempt; the result carries
* `attempts`.
*/
export async function observeArrivalRotationWithRetry({
prepare = async () => {},
observe = observeArrivalRotation,
maxAttempts = ARRIVAL_MAX_ATTEMPTS,
...options
}) {
let observed;
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
await prepare();
observed = { ...(await observe(options)), attempts: attempt };
if (!observed.error || arrivalAngleViolated(observed)) break;
}
return observed;
}
/** Test the captured frames without retrying a bad first observation. */
export function arrivalRotationPassed(observation) {
return (
!!observation &&
!observation.error &&
observation.shown === true &&
observation.elapsedMs < ARRIVAL_OBSERVATION_MS &&
(observation.dMoveEnd * 180) / Math.PI < 10 &&
(observation.dReveal * 180) / Math.PI < 10
observation.moveEndMs < ARRIVAL_OBSERVATION_MS &&
observation.revealMs < ARRIVAL_OBSERVATION_MS &&
Number.isFinite(observation.dMoveEnd) &&
Number.isFinite(observation.dReveal) &&
!arrivalAngleViolated(observation)
);
}
+231 -8
View File
@@ -1,19 +1,23 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import * as arrivalModule from './trackArrivalObservation.mjs';
import {
ARRIVAL_OBSERVATION_MS,
arrivalAngleViolated,
arrivalRotationPassed,
observeArrivalRotation,
observeArrivalRotationWithRetry,
} from './trackArrivalObservation.mjs';
function fixture() {
function fixture(start = observeArrivalRotation) {
let time = 0;
let state = { shown: true, rotationError: 0 };
const listeners = new Set();
const timers = new Map();
let renders = 0;
let moveEnds = 0;
const observation = observeArrivalRotation({
const observation = start({
scene: {
postRender: {
addEventListener(listener) {
@@ -110,7 +114,8 @@ test('no reveal fails at a deadline before the one-second idle catch-up', async
assert.equal(ARRIVAL_OBSERVATION_MS, 500);
f.frame(8, { shown: true, rotationError: 0 });
f.frame(480);
f.expire(500);
// The reveal budget is measured from the 8 ms settle frame.
f.expire(508);
const result = await f.observation;
assert.match(result.error, /reveal.*500 ms/);
assert.equal(arrivalRotationPassed(result), false);
@@ -123,20 +128,46 @@ test('no reveal fails at a deadline before the one-second idle catch-up', async
test('a late visible frame fails even if the timeout callback is delayed', async () => {
const f = fixture();
f.frame(8, { shown: true, rotationError: 0 });
f.frame(501, { shown: true, rotationError: 0 });
f.frame(509, { shown: true, rotationError: 0 });
const result = await f.observation;
assert.match(result.error, /reveal.*500 ms/);
assert.equal(result.elapsedMs, 501);
assert.equal(result.elapsedMs, 509);
assert.equal(arrivalRotationPassed(result), false);
});
test('the deadline includes moveEnd setup rather than restarting when hidden', async () => {
test('a slow software renderer passes when each frame stays inside its own budget', async () => {
// Headless SwiftShader frames run ~300 ms: settle lands at 300 ms and the
// reveal at 600 ms from moveEnd. Both stay inside the 1 s idle refresh.
const f = fixture();
f.frame(400, { shown: true, rotationError: 0 });
f.frame(300, { shown: true, rotationError: 0 });
f.expire(500); // the settle frame re-armed the deadline to 800 ms
f.frame(600, { shown: true, rotationError: 0 });
const result = await f.observation;
assert.equal(result.moveEndMs, 400);
assert.equal(result.moveEndMs, 300);
assert.equal(result.revealMs, 300);
assert.equal(result.elapsedMs, 600);
assert.equal(result.flipFrame, 1);
assert.equal(arrivalRotationPassed(result), true);
assert.equal(f.resources.timers, 0);
});
test('a settle frame later than its budget fails as a moveEnd timeout', async () => {
const f = fixture();
f.frame(500, { shown: true, rotationError: 0 });
const result = await f.observation;
assert.match(result.error, /moveEnd.*500 ms/);
assert.equal(result.moveEndMs, null);
assert.equal(arrivalRotationPassed(result), false);
});
test('a reveal later than its own budget fails even after a fast settle frame', async () => {
const f = fixture();
f.frame(400, { shown: true, rotationError: 0 });
f.frame(901, { shown: true, rotationError: 0 });
const result = await f.observation;
assert.match(result.error, /reveal.*500 ms/);
assert.equal(result.moveEndMs, 400);
assert.equal(result.elapsedMs, 901);
assert.equal(arrivalRotationPassed(result), false);
});
@@ -149,3 +180,195 @@ test('missing moveEnd frames fail without waiting for a render callback', async
assert.equal(f.resources.listeners, 0);
assert.equal(f.resources.timers, 0);
});
// Let a retry's next attempt register its postRender listener.
const settle = () => new Promise((resolve) => setImmediate(resolve));
function retryFixture(extra = {}) {
const counts = { prepares: 0 };
const f = fixture((options) =>
observeArrivalRotationWithRetry({
...options,
...extra,
async prepare() {
counts.prepares++;
},
}),
);
f.counts = counts;
return f;
}
test('a wrong settle angle followed by a reveal timeout is final, not retried into a pass', async () => {
const f = retryFixture();
await settle();
// 0.5 rad ≈ 29°: a real threshold violation on the settle frame.
f.frame(8, { shown: true, rotationError: 0.5 });
f.expire(508); // the reveal never comes inside its budget
await settle();
// An otherwise clean second attempt, which must never be started.
f.frame(516, { shown: true, rotationError: 0 });
f.frame(524, { shown: true, rotationError: 0 });
const result = await f.observation;
assert.match(result.error, /reveal.*500 ms/);
assert.equal(result.dMoveEnd, 0.5, 'the measured settle angle is kept');
assert.equal(arrivalAngleViolated(result), true);
assert.equal(result.attempts, 1);
assert.equal(f.counts.prepares, 1);
assert.equal(f.resources.moveEnds, 1);
assert.equal(arrivalRotationPassed(result), false);
assert.equal(f.resources.listeners, 0);
assert.equal(f.resources.timers, 0);
});
test('an inconclusive reveal timeout after a correct settle earns one more attempt', async () => {
const f = retryFixture();
await settle();
f.frame(8, { shown: true, rotationError: 0 });
f.expire(508);
await settle();
f.frame(516, { shown: true, rotationError: 0 });
f.frame(596, { shown: true, rotationError: 0 });
const result = await f.observation;
assert.equal(result.attempts, 2);
assert.equal(f.counts.prepares, 2);
assert.equal(f.resources.moveEnds, 2);
assert.equal(arrivalRotationPassed(result), true);
});
test('timeouts stop after the attempt limit and still fail', async () => {
const f = retryFixture();
await settle();
f.expire(500);
await settle();
f.expire(1000);
const result = await f.observation;
assert.match(result.error, /moveEnd.*500 ms/);
assert.equal(result.attempts, 2);
assert.equal(arrivalRotationPassed(result), false);
assert.equal(f.resources.listeners, 0);
assert.equal(f.resources.timers, 0);
});
test('a missing or non-finite angle never passes', () => {
const clean = {
shown: true,
moveEndMs: 8,
revealMs: 80,
dMoveEnd: 0,
dReveal: 0,
};
assert.equal(arrivalRotationPassed(clean), true);
for (const field of ['dMoveEnd', 'dReveal']) {
assert.equal(arrivalRotationPassed({ ...clean, [field]: NaN }), false);
assert.equal(
arrivalRotationPassed({ ...clean, [field]: undefined }),
false,
);
}
});
test('a wrong angle on a late reveal frame is final, not retried into a pass', async () => {
const f = retryFixture();
await settle();
f.frame(100, { shown: true, rotationError: 0 });
// Shown at 600 ms (500 ms after settle) still wearing the tampered nose.
f.frame(600, { shown: true, rotationError: 1.5 });
await settle();
f.frame(608, { shown: true, rotationError: 0 });
f.frame(688, { shown: true, rotationError: 0 });
const result = await f.observation;
assert.match(result.error, /reveal.*500 ms/);
assert.equal(result.dReveal, 1.5);
assert.equal(result.attempts, 1);
assert.equal(f.resources.moveEnds, 1);
assert.equal(arrivalRotationPassed(result), false);
});
test('a wrong angle on a late settle frame is final, not retried into a pass', async () => {
const f = retryFixture();
await settle();
f.frame(926, { shown: true, rotationError: 1.5 });
await settle();
f.frame(934, { shown: true, rotationError: 0 });
f.frame(1014, { shown: true, rotationError: 0 });
const result = await f.observation;
assert.match(result.error, /moveEnd.*500 ms/);
assert.equal(result.dMoveEnd, 1.5);
assert.equal(result.attempts, 1);
assert.equal(arrivalRotationPassed(result), false);
});
test('a late hidden reveal frame records no angle and stays retryable', async () => {
const f = retryFixture();
await settle();
f.frame(8, { shown: true, rotationError: 0 });
// Still hidden, still tampered: not a measured reveal angle.
f.frame(600, { shown: false });
const first = f.observation;
await settle();
f.frame(608, { shown: true, rotationError: 0 });
f.frame(688, { shown: true, rotationError: 0 });
const result = await first;
assert.equal(result.attempts, 2);
assert.equal(result.dReveal, 0);
assert.equal(arrivalRotationPassed(result), true);
});
test('a late frame with correct angles stays an inconclusive, retryable timeout', async () => {
const f = retryFixture();
await settle();
f.frame(926, { shown: true, rotationError: 0 });
await settle();
f.frame(934, { shown: true, rotationError: 0 });
f.frame(1014, { shown: true, rotationError: 0 });
const result = await f.observation;
assert.equal(result.attempts, 2);
assert.equal(arrivalRotationPassed(result), true);
});
test('a wrong first reveal on time is final: no retry', async () => {
const f = retryFixture();
await settle();
f.frame(8, { shown: true, rotationError: 0 });
f.frame(88, { shown: true, rotationError: 1.5 });
const result = await f.observation;
assert.equal(result.error, undefined);
assert.equal(result.attempts, 1);
assert.equal(f.resources.moveEnds, 1);
assert.equal(arrivalRotationPassed(result), false);
});
test('maxAttempts bounds inconclusive retries', async () => {
const f = retryFixture({ maxAttempts: 3 });
for (let attempt = 1; attempt <= 3; attempt++) {
await settle();
f.expire(attempt * 500);
}
const result = await f.observation;
assert.equal(result.attempts, 3);
assert.equal(f.counts.prepares, 3);
assert.equal(arrivalRotationPassed(result), false);
});
test('the 10° limit is inclusive', () => {
const limit = Math.PI / 18;
assert.equal(arrivalAngleViolated({ dMoveEnd: limit }), true);
assert.equal(arrivalAngleViolated({ dReveal: limit * 0.999 }), false);
});
test('every name the track harness imports from this module is exported', () => {
const harness = readFileSync(
new URL('../../scripts/track-regression.mjs', import.meta.url),
'utf8',
);
const names = [
...harness.matchAll(
/(?:import|const)\s*\{([^}]+)\}\s*(?:from\s*|=\s*await import\()[^;]*trackArrivalObservation\.mjs/g,
),
].flatMap((match) => match[1].split(',').map((name) => name.trim()));
assert.ok(names.includes('arrivalRotationPassed'));
assert.ok(names.includes('observeArrivalRotationWithRetry'));
for (const name of names)
assert.equal(typeof arrivalModule[name], 'function', name);
});