fix(observation): admit bounded LAB chunk coverage

This commit is contained in:
DCCONSTRUCTIONS 2026-07-23 22:34:07 +03:00
parent 082b7057da
commit 5a29a536a2
4 changed files with 104 additions and 14 deletions

View File

@ -46,6 +46,8 @@ export interface RerunPlaybackState {
playing: boolean; playing: boolean;
/** Latest session-time value currently available to the browser receiver. */ /** Latest session-time value currently available to the browser receiver. */
bufferedEndNs: number | null; 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. */ /** Declared final session-time value, when the archive descriptor provides it. */
expectedEndNs: number | null; expectedEndNs: number | null;
/** Download progress in the closed interval 0..1, or null without a valid expectation. */ /** Download progress in the closed interval 0..1, or null without a valid expectation. */
@ -182,6 +184,7 @@ export function createRecordedOpenWatchdog<T>({
export interface RecordedPlaybackBufferState { export interface RecordedPlaybackBufferState {
bufferedEndNs: number | null; bufferedEndNs: number | null;
expectedStartNs: number | null;
expectedEndNs: number | null; expectedEndNs: number | null;
bufferProgress: number | null; bufferProgress: number | null;
fullyBuffered: boolean; fullyBuffered: boolean;
@ -214,6 +217,7 @@ export function recordedPlaybackBufferState(
if (expectedTimelineEndSeconds === undefined) { if (expectedTimelineEndSeconds === undefined) {
return { return {
bufferedEndNs, bufferedEndNs,
expectedStartNs: null,
expectedEndNs: null, expectedEndNs: null,
bufferProgress: null, bufferProgress: null,
fullyBuffered: usableRange !== null, fullyBuffered: usableRange !== null,
@ -222,6 +226,7 @@ export function recordedPlaybackBufferState(
if (!Number.isFinite(expectedTimelineEndSeconds) || expectedTimelineEndSeconds < 0) { if (!Number.isFinite(expectedTimelineEndSeconds) || expectedTimelineEndSeconds < 0) {
return { return {
bufferedEndNs, bufferedEndNs,
expectedStartNs: null,
expectedEndNs: null, expectedEndNs: null,
bufferProgress: null, bufferProgress: null,
fullyBuffered: false, fullyBuffered: false,
@ -240,21 +245,32 @@ export function recordedPlaybackBufferState(
) { ) {
return { return {
bufferedEndNs, bufferedEndNs,
expectedStartNs: null,
expectedEndNs: null, expectedEndNs: null,
bufferProgress: null, bufferProgress: null,
fullyBuffered: false, 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 && const fullyBuffered = usableRange !== null &&
Math.abs(usableRange.min - expectedStartNs) <= BUFFER_END_TOLERANCE_NS && usableRange.min <= expectedStartNs + BUFFER_END_TOLERANCE_NS &&
Math.abs(usableRange.max - expectedEndNs) <= BUFFER_END_TOLERANCE_NS; usableRange.max >= expectedEndNs - BUFFER_END_TOLERANCE_NS;
const expectedDurationNs = expectedEndNs - expectedStartNs; const expectedDurationNs = expectedEndNs - expectedStartNs;
const bufferProgress = bufferedEndNs === null const bufferProgress = bufferedEndNs === null
? 0 ? 0
: expectedDurationNs <= 0 : expectedDurationNs <= 0
? (fullyBuffered ? 1 : 0) ? (fullyBuffered ? 1 : 0)
: Math.min(1, Math.max(0, (bufferedEndNs - expectedStartNs) / expectedDurationNs)); : 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. */ /** A recorded viewport is publishable only after its declared range is complete. */
@ -272,9 +288,19 @@ export function recordedPlaybackRangeWhenReady(
buffer: RecordedPlaybackBufferState, buffer: RecordedPlaybackBufferState,
artifactVerified: boolean, artifactVerified: boolean,
): { min: number; max: number } | null { ): { min: number; max: number } | null {
return artifactVerified && buffer.fullyBuffered && isUsableRecordedPlaybackRange(rangeNs) if (
? rangeNs !artifactVerified ||
: null; !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. */ /** Do not mount host timeline controls while a recorded generation is partial. */
@ -1076,6 +1102,7 @@ export function RerunViewport({
...(followLive ...(followLive
? { ? {
bufferedEndNs: rangeNs?.max ?? null, bufferedEndNs: rangeNs?.max ?? null,
expectedStartNs: null,
expectedEndNs: null, expectedEndNs: null,
bufferProgress: null, bufferProgress: null,
fullyBuffered: true, fullyBuffered: true,
@ -1096,7 +1123,7 @@ export function RerunViewport({
emitPlayback({ playing }); emitPlayback({ playing });
}, },
jumpToEnd: () => { jumpToEnd: () => {
const range = viewer.get_time_range(event.recording_id, timeline); const range = playbackState?.rangeNs;
if (!range) return; if (!range) return;
viewer.set_current_time(event.recording_id, timeline, range.max); viewer.set_current_time(event.recording_id, timeline, range.max);
emitPlayback({ rangeNs: range, currentNs: range.max, playing: false }); emitPlayback({ rangeNs: range, currentNs: range.max, playing: false });
@ -1151,7 +1178,9 @@ export function RerunViewport({
viewer.set_playing(event.recording_id, true); viewer.set_playing(event.recording_id, true);
}, },
initialPlaybackStartSeconds === undefined initialPlaybackStartSeconds === undefined
? expectedTimelineStartSeconds === undefined
? undefined ? undefined
: expectedTimelineStartSeconds * 1_000_000_000
: initialPlaybackStartSeconds * 1_000_000_000, : initialPlaybackStartSeconds * 1_000_000_000,
); );
if (autoplayStarted) { if (autoplayStarted) {
@ -1167,6 +1196,7 @@ export function RerunViewport({
...(followLive ...(followLive
? { ? {
bufferedEndNs: rangeNs?.max ?? null, bufferedEndNs: rangeNs?.max ?? null,
expectedStartNs: null,
expectedEndNs: null, expectedEndNs: null,
bufferProgress: null, bufferProgress: null,
fullyBuffered: true, fullyBuffered: true,

View File

@ -133,6 +133,7 @@ test("session catalog decodes canonical snake_case into a path-free camelCase mo
durationSeconds: 1_450.744, durationSeconds: 1_450.744,
replayable: true, replayable: true,
preparation: null, preparation: null,
lab: null,
}]); }]);
assert.equal("raw_capture" in catalog.items[0], false); assert.equal("raw_capture" in catalog.items[0], false);
assert.equal("path" in catalog.items[0], false); assert.equal("path" in catalog.items[0], false);

View File

@ -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); const firstFrame = recordedPlaybackBufferState({ min: 0, max: 0 }, 20);
assert.deepEqual(firstFrame, { assert.deepEqual(firstFrame, {
bufferedEndNs: 0, bufferedEndNs: 0,
expectedStartNs: 0,
expectedEndNs: 20_000_000_000, expectedEndNs: 20_000_000_000,
bufferProgress: 0, bufferProgress: 0,
fullyBuffered: false, fullyBuffered: false,
@ -58,6 +59,7 @@ test("host timeline remains unmounted until the verified recording is fully read
currentNs: 0, currentNs: 0,
playing: false, playing: false,
bufferedEndNs: 5, bufferedEndNs: 5,
expectedStartNs: 0,
expectedEndNs: 10, expectedEndNs: 10,
bufferProgress: 0.5, bufferProgress: 0.5,
fullyBuffered: false, 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", () => { test("buffer progress grows independently and preserves the full-buffer tolerance", () => {
assert.deepEqual(recordedPlaybackBufferState({ min: 0, max: 5_000_000_000 }, 20), { assert.deepEqual(recordedPlaybackBufferState({ min: 0, max: 5_000_000_000 }, 20), {
bufferedEndNs: 5_000_000_000, bufferedEndNs: 5_000_000_000,
expectedStartNs: 0,
expectedEndNs: 20_000_000_000, expectedEndNs: 20_000_000_000,
bufferProgress: 0.25, bufferProgress: 0.25,
fullyBuffered: false, fullyBuffered: false,
@ -97,14 +100,37 @@ test("buffer progress grows independently and preserves the full-buffer toleranc
{ min: 0, max: 19_999_500_000 }, { min: 0, max: 19_999_500_000 },
); );
const wrongDeclaredStart = recordedPlaybackBufferState( const missingDeclaredStart = recordedPlaybackBufferState(
{ min: 5_000_000_000, max: 20_000_000_000 }, { min: 5_000_000_000, max: 20_000_000_000 },
20, 20,
0, 0,
); );
assert.equal(wrongDeclaredStart.bufferProgress, 1); assert.equal(missingDeclaredStart.bufferProgress, 1);
assert.equal(wrongDeclaredStart.fullyBuffered, false); assert.equal(missingDeclaredStart.fullyBuffered, false);
assert.equal(isRecordedPlaybackReady(true, true, wrongDeclaredStart), 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", () => { test("recorded autoplay waits for the full range and then runs exactly once", () => {

View File

@ -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 results, not detector, semantic or fusion inference. It is completed before
operator handoff. Warm UI openings reuse the resulting 7,882,010-byte Rerun operator handoff. Warm UI openings reuse the resulting 7,882,010-byte Rerun
overlay. The synchronized base recording contains exact range markers, so the 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 host timeline is intended to be a 59.962-second comparison window and cannot
static post-E21 tail. 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.42185729295.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 ## Evidence