mirror of
https://github.com/ruvnet/RuView.git
synced 2026-08-27 10:35:52 +00:00
fix(nlos): tighten evidence gates and stream replay
This commit is contained in:
4
.github/workflows/consumer-nlos-ci.yml
vendored
4
.github/workflows/consumer-nlos-ci.yml
vendored
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>",
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -1 +1 @@
|
||||
ee03d67e99978163b6181cb9849c2e111722edc528bbc0083b090e7ce1612080 manifest.json
|
||||
866ee10afeaf318143213bec075bb08d587357b8b36ea6929d5abe5900f1e446 manifest.json
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
{
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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"));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user