From d6ec9f8590ba72e75fb04e3014ed63b9fafd47c3 Mon Sep 17 00:00:00 2001 From: ruv Date: Sat, 22 Aug 2026 21:16:54 -0400 Subject: [PATCH] fix(nlos): tighten evidence gates and stream replay --- .github/workflows/consumer-nlos-ci.yml | 4 +- ...los-raw-transient-reproduction-pipeline.md | 3 +- ...-evidence-privacy-benchmark-metaharness.md | 5 +- .../consumer-nlos-acceptance-protocol.md | 8 +- .../.claude/skills/consumer-nlos/SKILL.md | 5 +- harness/ruview/.harness/manifest.json | 10 +- harness/ruview/.harness/manifest.sha256 | 2 +- harness/ruview/README.md | 7 +- harness/ruview/skills/consumer-nlos.md | 5 +- harness/ruview/src/nlos.js | 10 +- harness/ruview/test/nlos.test.mjs | 27 +++- ui/ios-nlos/App/ContentView.swift | 20 +++ .../src/__tests__/screens/NLOSScreen.test.tsx | 3 + .../__tests__/services/nlos.service.test.ts | 7 +- ui/mobile/src/screens/NLOSScreen/index.tsx | 21 +++ v2/crates/ruview-nlos/src/bin/ruview-nlos.rs | 122 +++++++++++++----- 16 files changed, 193 insertions(+), 66 deletions(-) diff --git a/.github/workflows/consumer-nlos-ci.yml b/.github/workflows/consumer-nlos-ci.yml index e0174981..0b585956 100644 --- a/.github/workflows/consumer-nlos-ci.yml +++ b/.github/workflows/consumer-nlos-ci.yml @@ -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: diff --git a/docs/adr/ADR-328-consumer-nlos-raw-transient-reproduction-pipeline.md b/docs/adr/ADR-328-consumer-nlos-raw-transient-reproduction-pipeline.md index 20d94c89..80f69640 100644 --- a/docs/adr/ADR-328-consumer-nlos-raw-transient-reproduction-pipeline.md +++ b/docs/adr/ADR-328-consumer-nlos-raw-transient-reproduction-pipeline.md @@ -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 diff --git a/docs/adr/ADR-331-consumer-nlos-evidence-privacy-benchmark-metaharness.md b/docs/adr/ADR-331-consumer-nlos-evidence-privacy-benchmark-metaharness.md index 55f20db4..a20cccdb 100644 --- a/docs/adr/ADR-331-consumer-nlos-evidence-privacy-benchmark-metaharness.md +++ b/docs/adr/ADR-331-consumer-nlos-evidence-privacy-benchmark-metaharness.md @@ -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 diff --git a/docs/research/consumer-nlos-acceptance-protocol.md b/docs/research/consumer-nlos-acceptance-protocol.md index 5b55175c..34a1ae93 100644 --- a/docs/research/consumer-nlos-acceptance-protocol.md +++ b/docs/research/consumer-nlos-acceptance-protocol.md @@ -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": "", + "sensor_model": "VL53L8CH", "transient_kind": "COMPACT_NORMALIZED_HISTOGRAM", "sensor_configured_max_hz": "", - "upstream_commit": "", + "upstream_commit": "15314de422a765a2d1b72ea7037dfafb2f908d7c", "protocol_sha256": "", "capture_manifest_sha256": "", "sensor_identity_sha256": "", diff --git a/harness/ruview/.claude/skills/consumer-nlos/SKILL.md b/harness/ruview/.claude/skills/consumer-nlos/SKILL.md index 7ad91283..87de0f80 100644 --- a/harness/ruview/.claude/skills/consumer-nlos/SKILL.md +++ b/harness/ruview/.claude/skills/consumer-nlos/SKILL.md @@ -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. diff --git a/harness/ruview/.harness/manifest.json b/harness/ruview/.harness/manifest.json index 80c4ff0f..a0ea7b1d 100644 --- a/harness/ruview/.harness/manifest.json +++ b/harness/ruview/.harness/manifest.json @@ -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": { diff --git a/harness/ruview/.harness/manifest.sha256 b/harness/ruview/.harness/manifest.sha256 index 25525046..bd14dd6c 100644 --- a/harness/ruview/.harness/manifest.sha256 +++ b/harness/ruview/.harness/manifest.sha256 @@ -1 +1 @@ -ee03d67e99978163b6181cb9849c2e111722edc528bbc0083b090e7ce1612080 manifest.json +866ee10afeaf318143213bec075bb08d587357b8b36ea6929d5abe5900f1e446 manifest.json diff --git a/harness/ruview/README.md b/harness/ruview/README.md index 4c998c60..dee4c5f5 100644 --- a/harness/ruview/README.md +++ b/harness/ruview/README.md @@ -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: diff --git a/harness/ruview/skills/consumer-nlos.md b/harness/ruview/skills/consumer-nlos.md index 7ad91283..87de0f80 100644 --- a/harness/ruview/skills/consumer-nlos.md +++ b/harness/ruview/skills/consumer-nlos.md @@ -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. diff --git a/harness/ruview/src/nlos.js b/harness/ruview/src/nlos.js index d0c12165..e05fc454 100644 --- a/harness/ruview/src/nlos.js +++ b/harness/ruview/src/nlos.js @@ -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', }, { diff --git a/harness/ruview/test/nlos.test.mjs b/harness/ruview/test/nlos.test.mjs index d5292e05..2de94f7e 100644 --- a/harness/ruview/test/nlos.test.mjs +++ b/harness/ruview/test/nlos.test.mjs @@ -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', () => { diff --git a/ui/ios-nlos/App/ContentView.swift b/ui/ios-nlos/App/ContentView.swift index 06c394f1..c90890c5 100644 --- a/ui/ios-nlos/App/ContentView.swift +++ b/ui/ios-nlos/App/ContentView.swift @@ -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()) diff --git a/ui/mobile/src/__tests__/screens/NLOSScreen.test.tsx b/ui/mobile/src/__tests__/screens/NLOSScreen.test.tsx index c4fd4f26..afafd65e 100644 --- a/ui/mobile/src/__tests__/screens/NLOSScreen.test.tsx +++ b/ui/mobile/src/__tests__/screens/NLOSScreen.test.tsx @@ -55,6 +55,9 @@ describe('NLOSScreen', () => { render(); 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', () => { diff --git a/ui/mobile/src/__tests__/services/nlos.service.test.ts b/ui/mobile/src/__tests__/services/nlos.service.test.ts index 8e6bb789..aba38330 100644 --- a/ui/mobile/src/__tests__/services/nlos.service.test.ts +++ b/ui/mobile/src/__tests__/services/nlos.service.test.ts @@ -21,6 +21,7 @@ class MockSocket { const NOW = 1_700_000_000_100; const BEARER_TOKEN = 'e'.repeat(32); +const activeServices = new Set(); 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); }); diff --git a/ui/mobile/src/screens/NLOSScreen/index.tsx b/ui/mobile/src/screens/NLOSScreen/index.tsx index 44fdaced..2efc6840 100644 --- a/ui/mobile/src/screens/NLOSScreen/index.tsx +++ b/ui/mobile/src/screens/NLOSScreen/index.tsx @@ -81,6 +81,19 @@ export const NLOSScreen = () => { + + SOFTWARE PREVIEW + + The software path is implemented and locally validated. Research readiness remains blocked on: + + OPEN · macOS native compilation + OPEN · Physical VL53L8CH reproduction at 27 fps or better + OPEN · Measured CSI fusion improvement of at least 25 percent + + Builds, simulators, and synthetic replay do not close hardware or measured-fusion evidence gates. + + + @@ -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', diff --git a/v2/crates/ruview-nlos/src/bin/ruview-nlos.rs b/v2/crates/ruview-nlos/src/bin/ruview-nlos.rs index 1e55fe68..5b7129a1 100644 --- a/v2/crates/ruview-nlos/src/bin/ruview-nlos.rs +++ b/v2/crates/ruview-nlos/src/bin/ruview-nlos.rs @@ -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> { println!("{}", serde_json::to_string_pretty(&report)?); } Command::ValidateTrack { input } => { - let count = read_jsonl::( - &input, - |frame| frame.validate(), - )?; + let count = read_jsonl::(&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> { - 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, Box> { let mut poses = Vec::new(); - read_jsonl::(path, |pose| { + read_jsonl::(path, |pose| { pose.validate()?; poses.push(*pose); Ok(()) @@ -383,13 +363,48 @@ fn spawn_synthetic(hub: NlosHub) { }); } -fn load_transients(path: &PathBuf) -> Result, Box> { - let mut frames = Vec::new(); - read_jsonl::(path, |frame| { - frames.push(as_offline_replay(frame.clone())?); +fn track_jsonl( + path: &PathBuf, + background_frames: usize, + mut output_writer: impl Write, +) -> Result> { + 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::(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( +fn read_jsonl( path: &PathBuf, - mut validate: impl FnMut(&T) -> Result<(), E>, + mut validate: impl FnMut(&T) -> Result<(), Box>, ) -> Result> 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( #[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")); + } }