fix(nlos): tighten evidence gates and stream replay

This commit is contained in:
ruv
2026-08-22 21:16:54 -04:00
parent 73ebd9f33e
commit d6ec9f8590
16 changed files with 193 additions and 66 deletions

View File

@@ -143,7 +143,7 @@ jobs:
uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262
- name: Swift protocol and security tests
run: swift test
run: swift test -Xswiftc -strict-concurrency=complete -Xswiftc -warnings-as-errors
- name: Unsigned iOS Simulator build
run: >-
@@ -153,6 +153,8 @@ jobs:
-sdk iphonesimulator
-destination 'generic/platform=iOS Simulator'
CODE_SIGNING_ALLOWED=NO
SWIFT_STRICT_CONCURRENCY=complete
SWIFT_TREAT_WARNINGS_AS_ERRORS=YES
build
metaharness:

View File

@@ -46,7 +46,8 @@ photon path, relay geometry, ambient-light behavior, or target reflectivity.
### 1. Reproduce upstream before modifying its inference state
Phase 1 uses a pinned commit of the upstream implementation and a documented,
Phase 1 uses upstream commit
`15314de422a765a2d1b72ea7037dfafb2f908d7c` and a documented,
upstream-compatible ST assembly with verified raw or compact-normalized
histogram access. The current reference adapter targets VL53L8CH framing, but
the acceptance record stores the observed board/silicon identity and never

View File

@@ -132,8 +132,9 @@ does not embed, sensitive captures. Required fields include:
1. exact schema, `LIVE_HARDWARE` provenance, `MEASURED` claim tag, L2 evidence
level, and `EXTERNAL` ground truth;
2. protocol-frozen-before-capture flag;
3. enrolled external ST VL53L8-series sensor-model label for v1, verified raw/CNH transient kind, full
upstream SHA-1, and SHA-256 digests for protocol, firmware/API, combined and
3. enrolled external ST VL53L8CH sensor-model label for v1, verified raw/CNH
transient kind, pinned upstream SHA-1, and SHA-256 digests for protocol,
firmware/API, combined and
CSI capture manifests, scoped enrolled identities, calibration and analysis;
4. zero synthetic frames and zero replay frames in the scored set;
5. independently verified CSI, a CSI-only ablation, at least one CSI source and

View File

@@ -84,10 +84,10 @@ Complete and sign this table before scored capture:
| Item | Frozen value |
|---|---|
| ST kit and sensor | exact board and silicon read from hardware; raw/CNH API compatibility witnessed; scoped enrollment/certificate reference stored in the restricted manifest rather than a guessable raw-serial hash |
| ST kit and sensor | VL53L8CH silicon read from hardware; raw/CNH API compatibility witnessed; scoped enrollment/certificate reference stored in the restricted manifest rather than a guessable raw-serial hash |
| Firmware | source/release, compiler/toolchain, binary SHA-256 |
| Sensor configuration | zones, bins, bin width, requested rate, integration/subsampling, ambient settings |
| Upstream baseline | `sidsoma/consumer-nlos` full commit SHA and clean/patch manifest |
| Upstream baseline | `sidsoma/consumer-nlos` commit `15314de422a765a2d1b72ea7037dfafb2f908d7c` and clean/patch manifest |
| RuView | full Git commit SHA; `ruview-nlos` crate feature/config digest |
| CSI nodes | authenticated IDs, hardware/firmware, channel/bandwidth/subcarrier configuration |
| Optical calibration | wall points/plane, direct-return masks/peaks, background, timestamps, SHA-256, expiry |
@@ -399,10 +399,10 @@ placeholders, not results:
"security_review_passed": true,
"los_exclusion_verified": true,
"independent_csi_verified": true,
"sensor_model": "<actual-enrolled-external-ST-VL53L8-series-model>",
"sensor_model": "VL53L8CH",
"transient_kind": "COMPACT_NORMALIZED_HISTOGRAM",
"sensor_configured_max_hz": "<frozen-number>",
"upstream_commit": "<full-40-hex-sha1>",
"upstream_commit": "15314de422a765a2d1b72ea7037dfafb2f908d7c",
"protocol_sha256": "<nonzero-64-hex>",
"capture_manifest_sha256": "<nonzero-64-hex>",
"sensor_identity_sha256": "<nonzero-64-hex-scoped-enrollment-reference>",

View File

@@ -18,10 +18,11 @@ WiFi CSI frame, or synthetic replay. Those inputs may provide pose, context,
RF likelihoods, or deterministic software tests, but they do not contain the
delayed optical multipath samples needed for NLOS inversion.
The supported reproduction target is a pinned, upstream-compatible ST assembly
The supported reproduction target is VL53L8CH on the pinned upstream commit
`15314de422a765a2d1b72ea7037dfafb2f908d7c`, using a compatible ST assembly
with verified raw or compact-normalized histogram access. Record the actual
board/silicon/firmware/API identity rather than inferring it from the kit name.
Pin the full upstream `sidsoma/consumer-nlos` commit, scoped enrollment
Verify the full upstream `sidsoma/consumer-nlos` commit, scoped enrollment
reference, firmware/configuration, calibration, capture manifests, analysis and
RuView commit before comparing results.

View File

@@ -13,7 +13,7 @@
".claude/settings.json": "57d03e8995363bd120fb6d515702967afd0bd557797051301ff8f8156c845824",
".claude/skills/calibrate-room/SKILL.md": "4b29c7c331f47acad3c0f51b3d3d8f5b5573e316e081bae71dbe21a47fa95240",
".claude/skills/cognitum-spaces/SKILL.md": "96ae42cc72ad31dbb2f34d59e874c4d15f2e55fc969cd1f610dc1b9a4138840e",
".claude/skills/consumer-nlos/SKILL.md": "de0c1efc1fa8b9ab143b8a9abcffe5f21974b05635f5f4b5b2066cb738e4c042",
".claude/skills/consumer-nlos/SKILL.md": "60296281d8f4d3827a228fc2c4c5baf31401baf0d25fb2a46aedd78c1a15aa8f",
".claude/skills/onboard/SKILL.md": "97ee71f0aa985cfc03bb8e764789bb55c4f9fd5dae10a116c1071eab85b5893f",
".claude/skills/provision-node/SKILL.md": "5f73823794ed5f0b25c102aa8b1bf2dd534a1ec468173d8330c2af0ca24f239c",
".claude/skills/train-pose/SKILL.md": "92aebd4423470eb10eabaee642ec3493284d98b7ae9785e0f34378c709746e65",
@@ -23,7 +23,7 @@
".mcp/servers.json": "fec6075400f8350d8075beac8306690355c4b015425bfd0e5f52966234e9d66f",
"CLAUDE.md": "73b3858f601251d7c6d2527d1eaf2befca841bf80e615280ddbf46d924f692f6",
"LICENSE": "631f94984f626818d42ecf717aa6e8e0afd4f9f355ca706bd2effafbd1416d06",
"README.md": "205d816eb03a577a5768490af8de23ba7214db7e6e2aae333c3faa2e633b72fd",
"README.md": "6263a3c835d829f1919a86949dcb0cccdad5f09b8f3bfacb6dce1c74b60c92a2",
"bin/cli.js": "df3033d1f369af819c1db6498d06c6b331ee9394608491a12b3cedb45b746266",
"brain/corpus/core.jsonl": "c0fb7b079ded157059b91601361429944697dae3cc42abc00dfe1a680986b0f4",
"flywheel/evaluations.json": "ac4ff1f897a2444870cd2b8ae8aee8b1578e61467aeca4db57893f41be98a572",
@@ -38,7 +38,7 @@
"scripts/verify-manifest.mjs": "6491a221762efcfeb3e749ecab243b204f17fd5bc871f3d4025597f31b8f0f10",
"skills/calibrate-room.md": "4b29c7c331f47acad3c0f51b3d3d8f5b5573e316e081bae71dbe21a47fa95240",
"skills/cognitum-spaces.md": "96ae42cc72ad31dbb2f34d59e874c4d15f2e55fc969cd1f610dc1b9a4138840e",
"skills/consumer-nlos.md": "de0c1efc1fa8b9ab143b8a9abcffe5f21974b05635f5f4b5b2066cb738e4c042",
"skills/consumer-nlos.md": "60296281d8f4d3827a228fc2c4c5baf31401baf0d25fb2a46aedd78c1a15aa8f",
"skills/onboard.md": "97ee71f0aa985cfc03bb8e764789bb55c4f9fd5dae10a116c1071eab85b5893f",
"skills/provision-node.md": "5f73823794ed5f0b25c102aa8b1bf2dd534a1ec468173d8330c2af0ca24f239c",
"skills/train-pose.md": "92aebd4423470eb10eabaee642ec3493284d98b7ae9785e0f34378c709746e65",
@@ -50,7 +50,7 @@
"src/hosts/codex.js": "d41ecd132ce2db7b47aad9cebbc020d70e6810d48c3554858d099ff2e8f6608b",
"src/hosts/index.js": "ab276c41ab722bcdf72c2d1649cecbb760ae05c41c1372aae4c2447aa7c11539",
"src/mcp-server.js": "8b2ee4b939b25c1b1f507b295a43a2ebad852b8bac9d31af9bf7fb39b181c12e",
"src/nlos.js": "7582c1984ac092c51df81f1339e50a662eddbdf961d3ef25d51d95d96f33b824",
"src/nlos.js": "53114965a5c3d4373cfc57172efd41890797d7451756933db1bef98659bd9ac1",
"src/policy.js": "9ddc08231c1d1c84412a2bea03219ca77972a6254ccda940b9ce26df88e7a7bc",
"src/process-runner.js": "49533b038044dfb8bc76ed01c030d06a9856ead0836157fb693e2a7d40f786d6",
"src/redact.js": "ebf1afff46341078706b0401838c53db043603586e280d51ece5cf1feba35189",
@@ -58,7 +58,7 @@
"src/spaces.js": "45ef786537cb2a446db5e926e5a1c10b73639d2767dec84611f914f78d4325eb",
"src/tools.js": "3bb10444cd420006b7d9ba74617d1e6627528bd042e102592690311852998611"
},
"filesDigest": "44b60d20bafd4b51ef2a550e3437cb3a87f0774be60f46f284f588aaf1210e21",
"filesDigest": "8bb75d65e65ebf4d78002919d8b7cc06d7422c11d5c089ce72f4a3681634911a",
"brainDigest": "c0fb7b079ded157059b91601361429944697dae3cc42abc00dfe1a680986b0f4",
"gateFingerprint": "6e53c784eee38310188948fc75fb49e6b4ebc04e247d01b903fa8c8a92d67bdd",
"developmentPins": {

View File

@@ -1 +1 @@
ee03d67e99978163b6181cb9849c2e111722edc528bbc0083b090e7ce1612080 manifest.json
866ee10afeaf318143213bec075bb08d587357b8b36ea6929d5abe5900f1e446 manifest.json

View File

@@ -68,9 +68,10 @@ checkout: the environment is allowlisted and tails redacted, but Cargo/Swift/
Jest/Expo execution is not a sandbox.
The research gate accepts only a preregistered `LIVE_HARDWARE` record with an
external ground-truth capture manifest. It requires the LiDAR-only arm to run at
least 27 Hz and CSI fusion to reduce mean position error or lost-track rate by
at least 25 percent over 100 or more paired sequences, with adjusted confidence,
external ground-truth capture manifest. It requires the pinned VL53L8CH
baseline to run at least 27 Hz and CSI fusion to reduce mean position error or
lost-track rate by at least 25 percent over 100 or more paired sequences, with
adjusted confidence,
paired aggregate arithmetic, independent CSI ablation, frozen guardrails, and
witness/privacy/security report digests. `SYNTHETIC` and replay
frames can validate software, but can never pass that gate:

View File

@@ -18,10 +18,11 @@ WiFi CSI frame, or synthetic replay. Those inputs may provide pose, context,
RF likelihoods, or deterministic software tests, but they do not contain the
delayed optical multipath samples needed for NLOS inversion.
The supported reproduction target is a pinned, upstream-compatible ST assembly
The supported reproduction target is VL53L8CH on the pinned upstream commit
`15314de422a765a2d1b72ea7037dfafb2f908d7c`, using a compatible ST assembly
with verified raw or compact-normalized histogram access. Record the actual
board/silicon/firmware/API identity rather than inferring it from the kit name.
Pin the full upstream `sidsoma/consumer-nlos` commit, scoped enrollment
Verify the full upstream `sidsoma/consumer-nlos` commit, scoped enrollment
reference, firmware/configuration, calibration, capture manifests, analysis and
RuView commit before comparing results.

View File

@@ -13,6 +13,8 @@ import { redact } from './redact.js';
export const NLOS_EVIDENCE_SCHEMA = 'ruview.nlos.acceptance.v1';
export const NLOS_TRACK_SCHEMA = 'ruview.nlos.track.v1';
export const NLOS_REFERENCE_SENSOR_MODEL = 'VL53L8CH';
export const NLOS_UPSTREAM_COMMIT = '15314de422a765a2d1b72ea7037dfafb2f908d7c';
const MAX_EVIDENCE_BYTES = 1024 * 1024;
const MAX_SURFACE_FILE_BYTES = 4 * 1024 * 1024;
@@ -466,8 +468,8 @@ export function evaluateResearchEvidence(evidence) {
'los_exclusion_verified', 'independent_csi_verified',
]) if (evidence[field] !== true) findings.push(`${field}:must-be-true`);
if (!SAFE_LABEL.test(String(evidence.sensor_model || ''))) findings.push('sensor_model:invalid');
if (!/^VL53L8[A-Za-z0-9._:+-]*$/.test(String(evidence.sensor_model || ''))) {
findings.push('sensor_model:v1-requires-enrolled-external-ST-VL53L8-series');
if (evidence.sensor_model !== NLOS_REFERENCE_SENSOR_MODEL) {
findings.push(`sensor_model:v1-requires-${NLOS_REFERENCE_SENSOR_MODEL}`);
}
if (evidence.transient_kind !== 'COMPACT_NORMALIZED_HISTOGRAM'
&& evidence.transient_kind !== 'RAW_PHOTON_HISTOGRAM') {
@@ -480,6 +482,8 @@ export function evaluateResearchEvidence(evidence) {
if (!GIT_SHA1.test(String(evidence.upstream_commit || ''))
|| ZERO_GIT_SHA1.test(String(evidence.upstream_commit || ''))) {
findings.push('upstream_commit:expected-nonzero-full-sha1');
} else if (evidence.upstream_commit !== NLOS_UPSTREAM_COMMIT) {
findings.push('upstream_commit:must-match-pinned-baseline');
}
for (const field of [
'protocol_sha256', 'capture_manifest_sha256', 'sensor_identity_sha256',
@@ -650,7 +654,7 @@ export function makeNlosPlan({ repoRoot = null } = {}) {
phases: [
{
id: 1, name: 'Upstream reproduction',
input: 'Pinned upstream-compatible ST kit with verified compact normalized histogram access and recorded silicon identity',
input: `Pinned ${NLOS_REFERENCE_SENSOR_MODEL} baseline at ${NLOS_UPSTREAM_COMMIT} with verified compact normalized histogram access and recorded silicon identity`,
exit: 'LIVE_HARDWARE hidden-target tracking at >=27 Hz with frozen capture provenance',
},
{

View File

@@ -4,12 +4,17 @@ import assert from 'node:assert/strict';
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs';
import { join } from 'node:path';
import { tmpdir } from 'node:os';
import { evaluateResearchEvidence, NLOS_EVIDENCE_SCHEMA, NLOS_TRACK_SCHEMA } from '../src/nlos.js';
import {
evaluateResearchEvidence,
NLOS_EVIDENCE_SCHEMA,
NLOS_REFERENCE_SENSOR_MODEL,
NLOS_TRACK_SCHEMA,
NLOS_UPSTREAM_COMMIT,
} from '../src/nlos.js';
import { runTool } from '../src/tools.js';
import { run as cliRun } from '../bin/cli.js';
const DIGEST = 'a'.repeat(64);
const UPSTREAM_COMMIT = '0123456789abcdef0123456789abcdef01234567';
function arm(updateHz, positionErrorM, lostTrackRate) {
const evaluableDurationS = 100;
@@ -50,10 +55,10 @@ function passingEvidence(overrides = {}) {
security_review_passed: true,
los_exclusion_verified: true,
independent_csi_verified: true,
sensor_model: 'VL53L8CH',
sensor_model: NLOS_REFERENCE_SENSOR_MODEL,
transient_kind: 'COMPACT_NORMALIZED_HISTOGRAM',
sensor_configured_max_hz: 30,
upstream_commit: UPSTREAM_COMMIT,
upstream_commit: NLOS_UPSTREAM_COMMIT,
protocol_sha256: DIGEST,
capture_manifest_sha256: DIGEST,
sensor_identity_sha256: DIGEST,
@@ -179,13 +184,21 @@ test('research evidence rejects unknown and incomplete fields', () => {
assert.ok(bounded.findings.includes('sensor_identity_sha256:invalid-or-zero'));
assert.ok(bounded.findings.includes('lidar_only.update_hz:above-maximum'));
const appleClaim = evaluateResearchEvidence(passingEvidence({ sensor_model: 'iPhone15Pro' }));
assert.equal(appleClaim.pass, false);
assert.ok(appleClaim.findings.includes('sensor_model:v1-requires-enrolled-external-ST-VL53L8-series'));
for (const sensorModel of ['VL53L8CX', 'iPhone15Pro']) {
const wrongSensor = evaluateResearchEvidence(passingEvidence({ sensor_model: sensorModel }));
assert.equal(wrongSensor.pass, false);
assert.ok(wrongSensor.findings.includes('sensor_model:v1-requires-VL53L8CH'));
}
const zeroCommit = evaluateResearchEvidence(passingEvidence({ upstream_commit: '0'.repeat(40) }));
assert.equal(zeroCommit.pass, false);
assert.ok(zeroCommit.findings.includes('upstream_commit:expected-nonzero-full-sha1'));
const wrongCommit = evaluateResearchEvidence(passingEvidence({
upstream_commit: '0123456789abcdef0123456789abcdef01234567',
}));
assert.equal(wrongCommit.pass, false);
assert.ok(wrongCommit.findings.includes('upstream_commit:must-match-pinned-baseline'));
});
test('research evidence recomputes loss and requires powered paired endpoint coverage', () => {

View File

@@ -11,6 +11,7 @@ struct ContentView: View {
NavigationStack {
ScrollView {
VStack(spacing: 18) {
maturityCard
connectionCard
statusCard
visualizationCard
@@ -29,6 +30,19 @@ struct ContentView: View {
}
}
private var maturityCard: some View {
card(title: "Software preview") {
Text("The software path is implemented and locally validated. Research readiness remains blocked on these evidence gates:")
.font(.subheadline)
maturityGate("macOS native compilation")
maturityGate("Physical VL53L8CH reproduction at 27 fps or better")
maturityGate("Measured CSI fusion improvement of at least 25 percent")
Text("Builds, simulators, and synthetic replay do not close hardware or measured-fusion evidence gates.")
.font(.caption.bold())
.foregroundStyle(.secondary)
}
}
private var connectionCard: some View {
card(title: "Authenticated stream") {
TextField("wss://host.example/api/v1/nlos/ws", text: $model.endpointText)
@@ -229,6 +243,12 @@ struct ContentView: View {
.font(.subheadline)
}
private func maturityGate(_ title: String) -> some View {
Label("OPEN · \(title)", systemImage: "circle.dashed")
.font(.subheadline)
.foregroundStyle(.orange)
}
private func badge(_ text: String, color: Color) -> some View {
Text(text)
.font(.caption2.bold().monospaced())

View File

@@ -55,6 +55,9 @@ describe('NLOSScreen', () => {
render(<ThemeProvider><NLOSScreen /></ThemeProvider>);
expect(screen.getByText('RuView NLOS')).toBeTruthy();
expect(screen.getByText(/does not access raw iPhone LiDAR timing data/)).toBeTruthy();
expect(screen.getByTestId('nlos-maturity-boundary')).toBeTruthy();
expect(screen.getByText(/Physical VL53L8CH reproduction at 27 fps or better/)).toBeTruthy();
expect(screen.getByText(/Measured CSI fusion improvement of at least 25 percent/)).toBeTruthy();
});
it('always watermarks synthetic replay', () => {

View File

@@ -21,6 +21,7 @@ class MockSocket {
const NOW = 1_700_000_000_100;
const BEARER_TOKEN = 'e'.repeat(32);
const activeServices = new Set<NlosService>();
const createHarness = () => {
const socket = new MockSocket();
@@ -42,7 +43,9 @@ const createHarness = () => {
setTimeout: globalThis.setTimeout.bind(globalThis),
clearTimeout: globalThis.clearTimeout.bind(globalThis),
};
return { service: new NlosService(dependencies), socket, fetchMock, dependencies };
const service = new NlosService(dependencies);
activeServices.add(service);
return { service, socket, fetchMock, dependencies };
};
const authenticate = (socket: MockSocket) => {
@@ -57,6 +60,8 @@ const authenticate = (socket: MockSocket) => {
describe('NlosService', () => {
afterEach(() => {
for (const service of activeServices) service.disconnect();
activeServices.clear();
configureNlosBearerToken(null);
});

View File

@@ -81,6 +81,19 @@ export const NLOSScreen = () => {
</ThemedText>
</View>
<View testID="nlos-maturity-boundary" style={styles.maturityCard}>
<ThemedText preset="labelMd" style={{ color: colors.accent }}>SOFTWARE PREVIEW</ThemedText>
<ThemedText preset="bodySm">
The software path is implemented and locally validated. Research readiness remains blocked on:
</ThemedText>
<ThemedText preset="bodySm" color="textSecondary">OPEN · macOS native compilation</ThemedText>
<ThemedText preset="bodySm" color="textSecondary">OPEN · Physical VL53L8CH reproduction at 27 fps or better</ThemedText>
<ThemedText preset="bodySm" color="textSecondary">OPEN · Measured CSI fusion improvement of at least 25 percent</ThemedText>
<ThemedText preset="bodySm" color="textSecondary">
Builds, simulators, and synthetic replay do not close hardware or measured-fusion evidence gates.
</ThemedText>
</View>
<ProvenancePanel frame={frame} freshness={freshness} streamStatus={streamStatus} />
<View style={styles.visualizationCard}>
@@ -194,6 +207,14 @@ const styles = StyleSheet.create({
borderRadius: 10,
padding: spacing.md,
},
maturityCard: {
backgroundColor: colors.surface,
borderColor: colors.accentDim,
borderWidth: 1,
borderRadius: 12,
padding: spacing.md,
gap: spacing.sm,
},
visualizationCard: {
position: 'relative',
overflow: 'hidden',

View File

@@ -2,7 +2,7 @@
//! protocol; this binary never promotes its synthetic benchmark.
use std::fs::File;
use std::io::{BufRead, BufReader};
use std::io::{BufRead, BufReader, Write};
use std::net::{IpAddr, SocketAddr};
use std::path::PathBuf;
use std::time::Duration;
@@ -128,33 +128,14 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
println!("{}", serde_json::to_string_pretty(&report)?);
}
Command::ValidateTrack { input } => {
let count = read_jsonl::<TrackEnvelope, ruview_nlos::protocol::ContractError>(
&input,
|frame| frame.validate(),
)?;
let count = read_jsonl::<TrackEnvelope>(&input, |frame| Ok(frame.validate()?))?;
println!("validated {count} bounded track frames");
}
Command::TrackJsonl {
input,
background_frames,
} => {
let frames = load_transients(&input)?;
if background_frames < 2 || background_frames >= frames.len() {
return Err("background-frames must leave at least one tracking frame".into());
}
let calibration = Calibration::from_background(
&frames[..background_frames],
CalibrationConfig::default(),
)?;
let mut tracker = MotionApertureTracker::new(
calibration,
CanonicalObject::point(),
TrackerConfig::default(),
)?;
for frame in &frames[background_frames..] {
let output = tracker.update(frame, None)?;
println!("{}", serde_json::to_string(&output)?);
}
track_jsonl(&input, background_frames, std::io::stdout().lock())?;
}
#[cfg(feature = "hardware")]
Command::CaptureSt {
@@ -236,7 +217,6 @@ fn capture_st(
start_bin: u16,
frequency_hz: u16,
) -> Result<(), Box<dyn std::error::Error>> {
use std::io::Write;
use std::time::{Instant, SystemTime, UNIX_EPOCH};
if !(1..=1_000_000).contains(&frame_limit) || !(1..=30).contains(&frequency_hz) {
@@ -341,7 +321,7 @@ fn valid_cli_label(value: &str) -> bool {
#[cfg(feature = "hardware")]
fn load_sensor_poses(path: &PathBuf) -> Result<Vec<SensorPose>, Box<dyn std::error::Error>> {
let mut poses = Vec::new();
read_jsonl::<SensorPose, ruview_nlos::protocol::ContractError>(path, |pose| {
read_jsonl::<SensorPose>(path, |pose| {
pose.validate()?;
poses.push(*pose);
Ok(())
@@ -383,13 +363,48 @@ fn spawn_synthetic(hub: NlosHub) {
});
}
fn load_transients(path: &PathBuf) -> Result<Vec<TransientFrame>, Box<dyn std::error::Error>> {
let mut frames = Vec::new();
read_jsonl::<TransientFrame, ruview_nlos::protocol::ContractError>(path, |frame| {
frames.push(as_offline_replay(frame.clone())?);
fn track_jsonl(
path: &PathBuf,
background_frames: usize,
mut output_writer: impl Write,
) -> Result<usize, Box<dyn std::error::Error>> {
let calibration_config = CalibrationConfig::default();
if !(2..=calibration_config.max_background_frames).contains(&background_frames) {
return Err("background-frames must be between 2 and 256".into());
}
let mut background = Vec::with_capacity(background_frames);
let mut tracker = None;
let mut tracked_frames = 0_usize;
read_jsonl::<TransientFrame>(path, |frame| {
let frame = as_offline_replay(frame.clone())?;
if background.len() < background_frames {
background.push(frame);
return Ok(());
}
if tracker.is_none() {
let calibration_frames = std::mem::take(&mut background);
let calibration =
Calibration::from_background(&calibration_frames, calibration_config)?;
tracker = Some(MotionApertureTracker::new(
calibration,
CanonicalObject::point(),
TrackerConfig::default(),
)?);
}
let tracked = tracker
.as_mut()
.ok_or("tracker initialization failed")?
.update(&frame, None)?;
serde_json::to_writer(&mut output_writer, &tracked)?;
output_writer.write_all(b"\n")?;
tracked_frames = tracked_frames.saturating_add(1);
Ok(())
})?;
Ok(frames)
if tracked_frames == 0 {
return Err("background-frames must leave at least one tracking frame".into());
}
Ok(tracked_frames)
}
fn as_offline_replay(
@@ -406,13 +421,12 @@ fn as_offline_replay(
Ok(frame)
}
fn read_jsonl<T, E>(
fn read_jsonl<T>(
path: &PathBuf,
mut validate: impl FnMut(&T) -> Result<(), E>,
mut validate: impl FnMut(&T) -> Result<(), Box<dyn std::error::Error>>,
) -> Result<usize, Box<dyn std::error::Error>>
where
T: serde::de::DeserializeOwned,
E: std::error::Error + 'static,
{
let metadata = std::fs::metadata(path)?;
if metadata.len() > 512 * 1024 * 1024 {
@@ -461,9 +475,12 @@ fn read_bounded_line<R: BufRead>(
#[cfg(test)]
mod tests {
use super::{as_offline_replay, read_bounded_line};
use super::{as_offline_replay, read_bounded_line, track_jsonl};
use ruview_nlos::{EvidenceLevel, FrameSource, SyntheticScene, TransientKind, Vec3};
use std::io::Cursor;
use std::fs::{remove_file, File};
use std::io::{Cursor, Write};
use std::path::PathBuf;
use std::time::{SystemTime, UNIX_EPOCH};
#[test]
fn bounded_reader_rejects_before_growing_past_limit() {
@@ -493,4 +510,41 @@ mod tests {
assert_eq!(replay.provenance.transient_kind, TransientKind::Replay);
assert_eq!(replay.provenance.transport, "replay");
}
#[test]
fn track_jsonl_streams_after_bounded_background_window() {
let nonce = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_nanos();
let path = std::env::temp_dir().join(format!(
"ruview-nlos-track-{}-{nonce}.jsonl",
std::process::id()
));
let mut scene = SyntheticScene::default();
let mut frames = scene.background_frames(2);
frames.push(scene.frame(Some(Vec3::new(0.1, 0.0, 1.0)), 1.0, 2));
let mut input = File::create(&path).unwrap();
for frame in frames {
serde_json::to_writer(&mut input, &frame).unwrap();
input.write_all(b"\n").unwrap();
}
drop(input);
let mut output = Vec::new();
let tracked = track_jsonl(&path, 2, &mut output).unwrap();
remove_file(&path).unwrap();
assert_eq!(tracked, 1);
assert_eq!(output.iter().filter(|byte| **byte == b'\n').count(), 1);
let envelope: serde_json::Value = serde_json::from_slice(&output).unwrap();
assert_eq!(envelope["source"], "synthetic");
}
#[test]
fn track_jsonl_rejects_unbounded_calibration_window_before_io() {
let error =
track_jsonl(&PathBuf::from("does-not-exist.jsonl"), 257, Vec::new()).unwrap_err();
assert!(error.to_string().contains("between 2 and 256"));
}
}