From 5a29a536a2f4b385f84b8bd7bf627dbc1b399fda Mon Sep 17 00:00:00 2001 From: DCCONSTRUCTIONS Date: Thu, 23 Jul 2026 22:34:07 +0300 Subject: [PATCH] fix(observation): admit bounded LAB chunk coverage --- .../src/components/RerunViewport.tsx | 46 +++++++++++++++---- .../test/observationSessions.test.mjs | 1 + .../rerunViewportProgressivePlayback.test.mjs | 34 ++++++++++++-- .../perception/LAB_E21_REPORT_2026-07-23.md | 37 ++++++++++++++- 4 files changed, 104 insertions(+), 14 deletions(-) diff --git a/apps/control-station/src/components/RerunViewport.tsx b/apps/control-station/src/components/RerunViewport.tsx index 78a7f38..b55ed71 100644 --- a/apps/control-station/src/components/RerunViewport.tsx +++ b/apps/control-station/src/components/RerunViewport.tsx @@ -46,6 +46,8 @@ export interface RerunPlaybackState { playing: boolean; /** Latest session-time value currently available to the browser receiver. */ bufferedEndNs: number | null; + /** Declared first session-time value, when the archive descriptor provides it. */ + expectedStartNs: number | null; /** Declared final session-time value, when the archive descriptor provides it. */ expectedEndNs: number | null; /** Download progress in the closed interval 0..1, or null without a valid expectation. */ @@ -182,6 +184,7 @@ export function createRecordedOpenWatchdog({ export interface RecordedPlaybackBufferState { bufferedEndNs: number | null; + expectedStartNs: number | null; expectedEndNs: number | null; bufferProgress: number | null; fullyBuffered: boolean; @@ -214,6 +217,7 @@ export function recordedPlaybackBufferState( if (expectedTimelineEndSeconds === undefined) { return { bufferedEndNs, + expectedStartNs: null, expectedEndNs: null, bufferProgress: null, fullyBuffered: usableRange !== null, @@ -222,6 +226,7 @@ export function recordedPlaybackBufferState( if (!Number.isFinite(expectedTimelineEndSeconds) || expectedTimelineEndSeconds < 0) { return { bufferedEndNs, + expectedStartNs: null, expectedEndNs: null, bufferProgress: null, fullyBuffered: false, @@ -240,21 +245,32 @@ export function recordedPlaybackBufferState( ) { return { bufferedEndNs, + expectedStartNs: null, expectedEndNs: null, bufferProgress: null, fullyBuffered: false, }; } + // Rerun split preserves whole boundary chunks so that all generated splits + // still sum exactly to the source recording. A verified bounded artifact can + // therefore begin slightly before its declared operator window. Admission + // requires coverage of the declared window, not byte-chunk boundary equality. const fullyBuffered = usableRange !== null && - Math.abs(usableRange.min - expectedStartNs) <= BUFFER_END_TOLERANCE_NS && - Math.abs(usableRange.max - expectedEndNs) <= BUFFER_END_TOLERANCE_NS; + usableRange.min <= expectedStartNs + BUFFER_END_TOLERANCE_NS && + usableRange.max >= expectedEndNs - BUFFER_END_TOLERANCE_NS; const expectedDurationNs = expectedEndNs - expectedStartNs; const bufferProgress = bufferedEndNs === null ? 0 : expectedDurationNs <= 0 ? (fullyBuffered ? 1 : 0) : Math.min(1, Math.max(0, (bufferedEndNs - expectedStartNs) / expectedDurationNs)); - return { bufferedEndNs, expectedEndNs, bufferProgress, fullyBuffered }; + return { + bufferedEndNs, + expectedStartNs, + expectedEndNs, + bufferProgress, + fullyBuffered, + }; } /** A recorded viewport is publishable only after its declared range is complete. */ @@ -272,9 +288,19 @@ export function recordedPlaybackRangeWhenReady( buffer: RecordedPlaybackBufferState, artifactVerified: boolean, ): { min: number; max: number } | null { - return artifactVerified && buffer.fullyBuffered && isUsableRecordedPlaybackRange(rangeNs) - ? rangeNs - : null; + if ( + !artifactVerified || + !buffer.fullyBuffered || + !isUsableRecordedPlaybackRange(rangeNs) + ) return null; + return { + min: buffer.expectedStartNs === null + ? rangeNs.min + : Math.max(rangeNs.min, buffer.expectedStartNs), + max: buffer.expectedEndNs === null + ? rangeNs.max + : Math.min(rangeNs.max, buffer.expectedEndNs), + }; } /** Do not mount host timeline controls while a recorded generation is partial. */ @@ -1076,6 +1102,7 @@ export function RerunViewport({ ...(followLive ? { bufferedEndNs: rangeNs?.max ?? null, + expectedStartNs: null, expectedEndNs: null, bufferProgress: null, fullyBuffered: true, @@ -1096,7 +1123,7 @@ export function RerunViewport({ emitPlayback({ playing }); }, jumpToEnd: () => { - const range = viewer.get_time_range(event.recording_id, timeline); + const range = playbackState?.rangeNs; if (!range) return; viewer.set_current_time(event.recording_id, timeline, range.max); emitPlayback({ rangeNs: range, currentNs: range.max, playing: false }); @@ -1151,7 +1178,9 @@ export function RerunViewport({ viewer.set_playing(event.recording_id, true); }, initialPlaybackStartSeconds === undefined - ? undefined + ? expectedTimelineStartSeconds === undefined + ? undefined + : expectedTimelineStartSeconds * 1_000_000_000 : initialPlaybackStartSeconds * 1_000_000_000, ); if (autoplayStarted) { @@ -1167,6 +1196,7 @@ export function RerunViewport({ ...(followLive ? { bufferedEndNs: rangeNs?.max ?? null, + expectedStartNs: null, expectedEndNs: null, bufferProgress: null, fullyBuffered: true, diff --git a/apps/control-station/test/observationSessions.test.mjs b/apps/control-station/test/observationSessions.test.mjs index aa88ea9..dbb75fa 100644 --- a/apps/control-station/test/observationSessions.test.mjs +++ b/apps/control-station/test/observationSessions.test.mjs @@ -133,6 +133,7 @@ test("session catalog decodes canonical snake_case into a path-free camelCase mo durationSeconds: 1_450.744, replayable: true, preparation: null, + lab: null, }]); assert.equal("raw_capture" in catalog.items[0], false); assert.equal("path" in catalog.items[0], false); diff --git a/apps/control-station/test/rerunViewportProgressivePlayback.test.mjs b/apps/control-station/test/rerunViewportProgressivePlayback.test.mjs index a754faf..546affe 100644 --- a/apps/control-station/test/rerunViewportProgressivePlayback.test.mjs +++ b/apps/control-station/test/rerunViewportProgressivePlayback.test.mjs @@ -42,6 +42,7 @@ test("a first frame reports buffer telemetry but is not ready for presentation", const firstFrame = recordedPlaybackBufferState({ min: 0, max: 0 }, 20); assert.deepEqual(firstFrame, { bufferedEndNs: 0, + expectedStartNs: 0, expectedEndNs: 20_000_000_000, bufferProgress: 0, fullyBuffered: false, @@ -58,6 +59,7 @@ test("host timeline remains unmounted until the verified recording is fully read currentNs: 0, playing: false, bufferedEndNs: 5, + expectedStartNs: 0, expectedEndNs: 10, bufferProgress: 0.5, fullyBuffered: false, @@ -77,6 +79,7 @@ test("host timeline remains unmounted until the verified recording is fully read test("buffer progress grows independently and preserves the full-buffer tolerance", () => { assert.deepEqual(recordedPlaybackBufferState({ min: 0, max: 5_000_000_000 }, 20), { bufferedEndNs: 5_000_000_000, + expectedStartNs: 0, expectedEndNs: 20_000_000_000, bufferProgress: 0.25, fullyBuffered: false, @@ -97,14 +100,37 @@ test("buffer progress grows independently and preserves the full-buffer toleranc { min: 0, max: 19_999_500_000 }, ); - const wrongDeclaredStart = recordedPlaybackBufferState( + const missingDeclaredStart = recordedPlaybackBufferState( { min: 5_000_000_000, max: 20_000_000_000 }, 20, 0, ); - assert.equal(wrongDeclaredStart.bufferProgress, 1); - assert.equal(wrongDeclaredStart.fullyBuffered, false); - assert.equal(isRecordedPlaybackReady(true, true, wrongDeclaredStart), false); + assert.equal(missingDeclaredStart.bufferProgress, 1); + assert.equal(missingDeclaredStart.fullyBuffered, false); + assert.equal(isRecordedPlaybackReady(true, true, missingDeclaredStart), false); +}); + +test("a verified split boundary spill covers and clamps the declared LAB window", () => { + const actualRange = { + min: 34_756_228_209, + max: 95_383_857_292, + }; + const buffer = recordedPlaybackBufferState( + actualRange, + 95.383857292, + 35.421857292, + ); + + assert.equal(buffer.bufferProgress, 1); + assert.equal(buffer.fullyBuffered, true); + assert.equal(isRecordedPlaybackReady(true, true, buffer), true); + assert.deepEqual( + recordedPlaybackRangeWhenReady(actualRange, buffer, true), + { + min: 35_421_857_292, + max: 95_383_857_292, + }, + ); }); test("recorded autoplay waits for the full range and then runs exactly once", () => { diff --git a/experiments/perception/LAB_E21_REPORT_2026-07-23.md b/experiments/perception/LAB_E21_REPORT_2026-07-23.md index 374faf5..9a93e8e 100644 --- a/experiments/perception/LAB_E21_REPORT_2026-07-23.md +++ b/experiments/perception/LAB_E21_REPORT_2026-07-23.md @@ -222,8 +222,41 @@ The 144.54-second step is viewer-container publication from already computed results, not detector, semantic or fusion inference. It is completed before operator handoff. Warm UI openings reuse the resulting 7,882,010-byte Rerun overlay. The synchronized base recording contains exact range markers, so the -host timeline is a 59.962-second comparison window and cannot advance into a -static post-E21 tail. +host timeline is intended to be a 59.962-second comparison window and cannot +advance into a static post-E21 tail. + +### Operator admission regression and repair + +The first E21.3 browser handoff exposed one additional boundary condition. +Rerun `rrd split` preserves complete Arrow chunks so that all split outputs +still sum to the source recording. The verified bounded RRD therefore contains +a leading boundary chunk beginning at 34.756228209 seconds even though its +declared operator window begins at 35.421857292 seconds. Its final timestamp is +the declared 95.383857292 seconds. + +The original browser admission gate required both decoded endpoints to equal +the declared endpoints within 1 ms. Download and decode consequently reached +100%, but the scene could never be admitted because the harmless 665.629 ms +leading chunk spill failed the start equality check. + +The repaired contract admits a digest-verified RRD when its decoded range +*covers* the declared window. The host timeline and playback controller are +then clamped to the declared 35.421857292–95.383857292-second window, and +autoplay starts at the declared boundary rather than at the preserved chunk +edge. Missing coverage at either boundary still fails closed. + +Browser acceptance on the production build at `127.0.0.1:8000`: + +- three clean catalog-to-E21.3 openings completed; +- the second and third openings reached `Визуализатор готов` in about 0.5 s; +- no opening remained at `Загрузка и декодирование · 100%`; +- the operator timeline was exactly `00:00.000 / 00:59.962`; +- 2D objects, segmentation and 3D cuboids were enabled together and rendered + in the synchronized composition; +- camera/AI presentation changed while playback advanced; +- browser console errors after all three openings: 0; +- frontend unit tests: 151 / 151 passed; TypeScript check and production build + passed. ## Evidence