feat(mobile): refine spatial target UI #15
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Consumer NLOS | |
| on: | |
| push: | |
| branches: [main, 'feat/**', 'feature/**'] | |
| paths: | |
| - 'v2/crates/ruview-nlos/**' | |
| - 'ui/ios-nlos/**' | |
| - 'ui/mobile/**' | |
| - 'harness/ruview/**' | |
| - 'docs/adr/ADR-32[8-9]*' | |
| - 'docs/adr/ADR-33[0-1]*' | |
| - 'docs/adr/ADR-341-consumer-nlos-beta-tester-delivery-and-diagnostics.md' | |
| - 'docs/adr/ADR-342-cognitum-inspired-mobile-instrument-ui.md' | |
| - 'docs/research/consumer-nlos-acceptance-protocol.md' | |
| - 'docs/research/consumer-nlos-beta-test-protocol.md' | |
| - 'docs/screenshots/consumer-nlos-mobile-ui/**' | |
| - 'docs/schemas/ruview-nlos-*.schema.json' | |
| - 'docs/schemas/ruview-ios-visible-depth-diagnostic-v1.schema.json' | |
| - 'docs/security/consumer-nlos-threat-model.md' | |
| - '.github/workflows/consumer-nlos-ci.yml' | |
| - 'v2/Cargo.toml' | |
| - 'v2/Cargo.lock' | |
| pull_request: | |
| branches: [main] | |
| paths: | |
| - 'v2/crates/ruview-nlos/**' | |
| - 'ui/ios-nlos/**' | |
| - 'ui/mobile/**' | |
| - 'harness/ruview/**' | |
| - 'docs/**consumer-nlos*' | |
| - 'docs/adr/ADR-342-cognitum-inspired-mobile-instrument-ui.md' | |
| - 'docs/screenshots/consumer-nlos-mobile-ui/**' | |
| - 'docs/schemas/ruview-ios-visible-depth-diagnostic-v1.schema.json' | |
| - '.github/workflows/consumer-nlos-ci.yml' | |
| - 'v2/Cargo.toml' | |
| - 'v2/Cargo.lock' | |
| workflow_dispatch: | |
| permissions: | |
| contents: read | |
| jobs: | |
| rust-core: | |
| name: Rust core, security, and performance | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 | |
| with: | |
| submodules: recursive | |
| - name: Install Rust | |
| uses: dtolnay/rust-toolchain@4cda84d5c5c54efe2404f9d843567869ab1699d4 | |
| with: | |
| toolchain: '1.89' | |
| components: rustfmt, clippy | |
| - name: Cache Cargo | |
| uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 | |
| with: | |
| workspaces: v2 | |
| - name: Test dependency-free core | |
| working-directory: v2 | |
| run: cargo test -p ruview-nlos --no-default-features | |
| - name: Formatting and lint | |
| working-directory: v2 | |
| run: | | |
| cargo fmt --package ruview-nlos -- --check | |
| cargo clippy -p ruview-nlos --all-features --all-targets -- -D warnings | |
| - name: Test server and hardware adapters | |
| working-directory: v2 | |
| run: cargo test -p ruview-nlos --all-features | |
| - name: Check all targets | |
| working-directory: v2 | |
| run: cargo check -p ruview-nlos --all-features --all-targets | |
| - name: Audit Rust lockfile | |
| working-directory: v2 | |
| run: | | |
| cargo install cargo-audit --version 0.22.2 --locked | |
| cargo audit | |
| - name: Measure synthetic architecture gate | |
| working-directory: v2 | |
| run: | | |
| cargo run -p ruview-nlos --release -- benchmark --frames 300 --particles 1000 > /tmp/nlos-benchmark.json | |
| python - <<'PY' | |
| import json | |
| from pathlib import Path | |
| report = json.loads(Path('/tmp/nlos-benchmark.json').read_text()) | |
| assert report['evidence'] == 'SYNTHETIC_L0' | |
| assert report['hardwareReproductionGatePassed'] is False | |
| assert report['throughputFps'] >= 30.0, report | |
| assert report['lostTrackReductionPercent'] >= 25.0, report | |
| print(json.dumps(report, indent=2, sort_keys=True)) | |
| PY | |
| web-ios: | |
| name: Expo web iOS client | |
| runs-on: ubuntu-latest | |
| defaults: | |
| run: | |
| working-directory: ui/mobile | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 | |
| - name: Set up Node | |
| uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 | |
| with: | |
| node-version: '22' | |
| cache: npm | |
| cache-dependency-path: ui/mobile/package-lock.json | |
| - run: npm ci --ignore-scripts | |
| - run: npm test -- --runInBand | |
| - run: npx tsc --noEmit | |
| - run: npm run lint | |
| - name: Install pinned Playwright Chromium | |
| run: ./node_modules/.bin/playwright install --with-deps chromium | |
| - name: Run production browser E2E and reproduce mobile screenshots | |
| run: npm run e2e:web | |
| - name: Upload browser E2E review evidence | |
| if: ${{ !cancelled() }} | |
| uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 | |
| with: | |
| name: consumer-nlos-mobile-ui-browser-${{ github.sha }} | |
| path: | | |
| docs/screenshots/consumer-nlos-mobile-ui | |
| ui/mobile/test-results/playwright | |
| if-no-files-found: warn | |
| retention-days: 14 | |
| - name: Export browser bundle | |
| run: npx expo export --platform web | |
| - name: Export Expo iOS bundle | |
| run: npx expo export --platform ios --output-dir /tmp/ruview-expo-ios | |
| - name: Audit web dependency boundary | |
| run: | | |
| npm audit --json > /tmp/mobile-audit.json || true | |
| node <<'JS' | |
| const report = require('/tmp/mobile-audit.json'); | |
| const severe = Object.entries(report.vulnerabilities ?? {}) | |
| .filter(([, finding]) => ['high', 'critical'].includes(finding.severity)); | |
| if (severe.length) { | |
| console.error(JSON.stringify(severe, null, 2)); | |
| process.exit(1); | |
| } | |
| console.log(JSON.stringify(report.metadata?.vulnerabilities ?? {}, null, 2)); | |
| JS | |
| mobile-ui-contract: | |
| name: Mobile UI ADR, E2E, and screenshot contract | |
| runs-on: ubuntu-24.04 | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 | |
| - name: Validate UI-only architecture and deterministic review evidence | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| python - <<'PY' | |
| import re | |
| import struct | |
| from pathlib import Path | |
| adr_path = Path( | |
| "docs/adr/ADR-342-cognitum-inspired-mobile-instrument-ui.md" | |
| ) | |
| flow_path = Path("ui/mobile/e2e/nlos_mobile_ui.yaml") | |
| screenshot_dir = Path("docs/screenshots/consumer-nlos-mobile-ui") | |
| adr = adr_path.read_text(encoding="utf-8") | |
| required_sections = ( | |
| "## Context", | |
| "## Specification", | |
| "## Pseudocode and state transitions", | |
| "## Architecture", | |
| "## Decision", | |
| "## Alternatives with quantified tradeoffs", | |
| "## Accessibility", | |
| "## Security and privacy", | |
| "## Performance budgets", | |
| "## Refinement and failure handling", | |
| "## Requirement to evidence mapping", | |
| "## Rollout and rollback", | |
| "## Acceptance test", | |
| ) | |
| for section in required_sections: | |
| assert section in adr, f"missing ADR section: {section}" | |
| required_adr_contract = ( | |
| "UI-only", | |
| "feat/consumer-nlos-ruview", | |
| "Cognitum Explain", | |
| "No Cognitum name, logo, wordmark", | |
| "SYNTHETIC", | |
| "LIVE VERIFIED", | |
| "LIVE UNVERIFIED", | |
| "STALE", | |
| "DISCONNECTED", | |
| "390 by 844", | |
| "44 by 44 points", | |
| "under 1.5 seconds", | |
| "under 100 milliseconds", | |
| "zero runtime dependency", | |
| "Three.js", | |
| "physical iPhone validation pending", | |
| ) | |
| for phrase in required_adr_contract: | |
| assert phrase in adr, f"missing mobile UI contract: {phrase}" | |
| flow = flow_path.read_text(encoding="utf-8") | |
| required_flow_contract = ( | |
| "appId:", | |
| "---", | |
| "launchApp:", | |
| "tapOn:", | |
| "assertVisible:", | |
| "nlos-evidence-state", | |
| "nlos-synthetic-watermark", | |
| "nlos-beta-setup", | |
| "nlos-lidar-point-cloud", | |
| "nlos-cloud-target-count", | |
| ) | |
| for token in required_flow_contract: | |
| assert token in flow, f"missing Maestro UI contract token: {token}" | |
| screen_sources = "\n".join( | |
| path.read_text(encoding="utf-8") | |
| for path in sorted(Path("ui/mobile/src/screens/NLOSScreen").glob("*.tsx")) | |
| ) | |
| for test_id in ( | |
| "nlos-evidence-state", | |
| "nlos-synthetic-watermark", | |
| "nlos-beta-setup", | |
| ): | |
| assert test_id in screen_sources, f"missing screen contract: {test_id}" | |
| for state in ( | |
| "SYNTHETIC", | |
| "LIVE VERIFIED", | |
| "LIVE UNVERIFIED", | |
| "STALE", | |
| "DISCONNECTED", | |
| ): | |
| assert state in screen_sources, f"missing evidence state: {state}" | |
| state_type = re.search( | |
| r"export type NlosEvidenceState\s*=\s*(.*?);", | |
| screen_sources, | |
| re.DOTALL, | |
| ) | |
| assert state_type, "missing NlosEvidenceState type contract" | |
| actual_states = set(re.findall(r"'([^']+)'", state_type.group(1))) | |
| expected_states = { | |
| "SYNTHETIC", | |
| "LIVE VERIFIED", | |
| "LIVE UNVERIFIED", | |
| "STALE", | |
| "DISCONNECTED", | |
| } | |
| assert actual_states == expected_states, ( | |
| f"evidence state taxonomy drift: {sorted(actual_states)}" | |
| ) | |
| def png_dimensions(path): | |
| data = path.read_bytes() | |
| assert data.startswith(b"\x89PNG\r\n\x1a\n"), f"not a PNG: {path}" | |
| assert data[12:16] == b"IHDR", f"missing PNG IHDR: {path}" | |
| return struct.unpack(">II", data[16:24]) | |
| expected_screenshots = ( | |
| "overview-390x844.png", | |
| "synthetic-390x844.png", | |
| "setup-390x844.png", | |
| "point-cloud-390x844.png", | |
| ) | |
| for name in expected_screenshots: | |
| path = screenshot_dir / name | |
| assert path.is_file(), f"missing screenshot baseline: {path}" | |
| assert png_dimensions(path) == (390, 844), ( | |
| f"wrong screenshot dimensions for {path}: {png_dimensions(path)}" | |
| ) | |
| print( | |
| "Validated ADR 342, Maestro UI contract, five evidence states, " | |
| "and four 390x844 PNG baselines" | |
| ) | |
| PY | |
| native-ios: | |
| name: Native Swift, simulator, and unsigned archive dry gate | |
| runs-on: macos-26 | |
| defaults: | |
| run: | |
| working-directory: ui/ios-nlos | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 | |
| - name: Require Xcode 26 | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| xcodebuild -version | |
| xcode_major="$(xcodebuild -version | awk '/^Xcode / {split($2, version, "."); print version[1]}')" | |
| test "$xcode_major" = "26" | |
| - name: Swift protocol and security tests | |
| run: swift test | |
| - name: Unsigned iOS Simulator build | |
| run: >- | |
| xcodebuild | |
| -project RuViewNLOS.xcodeproj | |
| -scheme RuViewNLOS | |
| -sdk iphonesimulator | |
| -destination 'generic/platform=iOS Simulator' | |
| CODE_SIGNING_ALLOWED=NO | |
| SWIFT_STRICT_CONCURRENCY=complete | |
| SWIFT_SUPPRESS_WARNINGS=NO | |
| SWIFT_TREAT_WARNINGS_AS_ERRORS=YES | |
| build | |
| - name: Unsigned generic iOS archive dry gate | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| archive_path="$RUNNER_TEMP/RuViewNLOS.xcarchive" | |
| xcodebuild \ | |
| -project RuViewNLOS.xcodeproj \ | |
| -scheme RuViewNLOS \ | |
| -configuration Release \ | |
| -sdk iphoneos \ | |
| -destination 'generic/platform=iOS' \ | |
| -archivePath "$archive_path" \ | |
| CODE_SIGNING_ALLOWED=NO \ | |
| CODE_SIGNING_REQUIRED=NO \ | |
| SWIFT_STRICT_CONCURRENCY=complete \ | |
| SWIFT_SUPPRESS_WARNINGS=NO \ | |
| SWIFT_TREAT_WARNINGS_AS_ERRORS=YES \ | |
| archive | |
| test -f "$archive_path/Info.plist" | |
| if find "$archive_path" -name embedded.mobileprovision -print -quit | grep -q .; then | |
| echo "Unsigned dry archive unexpectedly contains a provisioning profile" >&2 | |
| exit 1 | |
| fi | |
| release-governance: | |
| name: Beta release governance contract | |
| runs-on: ubuntu-24.04 | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 | |
| - name: Validate ADR, protocol, workflow, and diagnostic boundary | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| python - <<'PY' | |
| import json | |
| from pathlib import Path | |
| adr_path = Path("docs/adr/ADR-341-consumer-nlos-beta-tester-delivery-and-diagnostics.md") | |
| protocol_path = Path("docs/research/consumer-nlos-beta-test-protocol.md") | |
| workflow_path = Path(".github/workflows/consumer-nlos-ci.yml") | |
| schema_path = Path("docs/schemas/ruview-ios-visible-depth-diagnostic-v1.schema.json") | |
| adr = adr_path.read_text(encoding="utf-8") | |
| protocol = protocol_path.read_text(encoding="utf-8") | |
| workflow = workflow_path.read_text(encoding="utf-8") | |
| schema = json.loads(schema_path.read_text(encoding="utf-8")) | |
| required_adr_sections = ( | |
| "## Specification", | |
| "## Pseudocode and state transitions", | |
| "## Architecture", | |
| "## Release architecture", | |
| "## Alternatives considered", | |
| "## Refinement and failure handling", | |
| "## Requirement-to-evidence mapping", | |
| "## Acceptance test", | |
| ) | |
| for section in required_adr_sections: | |
| assert section in adr, f"missing ADR section: {section}" | |
| required_contract = ( | |
| "under five minutes", | |
| "15 second calibration", | |
| "30 second wall scan", | |
| "65,536 bytes", | |
| "blocked_raw_transients_unavailable", | |
| "No raw capture", | |
| ) | |
| combined = adr + "\n" + protocol | |
| for phrase in required_contract: | |
| assert phrase in combined, f"missing beta contract: {phrase}" | |
| assert "runs-on: macos-26" in workflow | |
| assert 'test "$xcode_major" = "26"' in workflow | |
| assert "CODE_SIGNING_ALLOWED=NO" in workflow | |
| assert "CODE_SIGNING_REQUIRED=NO" in workflow | |
| assert ("sec" + "rets.") not in workflow | |
| assert ("upload" + "-app-store") not in workflow.lower() | |
| assert ("al" + "tool") not in workflow.lower() | |
| phase = { | |
| "phase": "calibration", | |
| "plannedDurationSeconds": 15, | |
| "observedDurationSeconds": 15.0, | |
| "frameCount": 450, | |
| "averageFPS": 30.0, | |
| "averageDepthCoverage": 0.91, | |
| "averageMovementMetersPerSecond": 0.08, | |
| "finalTrackingState": "normal", | |
| "peakThermalState": "nominal", | |
| } | |
| diagnostic = { | |
| "schema": "ruview.ios.visible-depth-diagnostic.v1", | |
| "sessionId": "00000000-0000-4000-8000-000000000000", | |
| "createdAt": "2026-08-23T00:00:00Z", | |
| "deviceModelFamily": "iPhone", | |
| "osVersion": "iOS 26.0", | |
| "appVersion": "1.0 (1)", | |
| "capabilities": { | |
| "worldTracking": True, | |
| "sceneDepth": True, | |
| "smoothedSceneDepth": True, | |
| "sceneMesh": True, | |
| "rawPhotonHistograms": False, | |
| }, | |
| "phases": [phase, {**phase, "phase": "wall_scan", "plannedDurationSeconds": 30}], | |
| "consent": { | |
| "localValidation": True, | |
| "diagnosticExport": True, | |
| "rawSensorExport": False, | |
| }, | |
| "evidenceLabel": "direct_depth", | |
| "physicalNLOSStatus": "blocked_raw_transients_unavailable", | |
| "cameraPermission": "granted", | |
| "completionStatus": "completed", | |
| } | |
| encoded = json.dumps( | |
| diagnostic, sort_keys=True, separators=(",", ":"), allow_nan=False | |
| ).encode("utf-8") | |
| assert len(encoded) <= 65_536 | |
| assert set(diagnostic) == set(schema["required"]) | |
| assert diagnostic["consent"]["rawSensorExport"] is False | |
| assert diagnostic["capabilities"]["rawPhotonHistograms"] is False | |
| assert diagnostic["physicalNLOSStatus"] == ( | |
| "blocked_raw_transients_unavailable" | |
| ) | |
| forbidden = { | |
| "image", "images", "rgb", "depth", "depth_map", "point_cloud", | |
| "csi", "transient", "histogram", "token", "authorization", | |
| "latitude", "longitude", "location", "trajectory", "raw", | |
| } | |
| def walk(value): | |
| if isinstance(value, dict): | |
| for key, child in value.items(): | |
| assert key.lower() not in forbidden, f"forbidden diagnostic key: {key}" | |
| walk(child) | |
| elif isinstance(value, list): | |
| for child in value: | |
| walk(child) | |
| walk(diagnostic) | |
| print(f"Validated beta governance contract and {len(encoded)} byte fixture") | |
| PY | |
| metaharness: | |
| name: Advisory MetaHarness | |
| runs-on: ubuntu-latest | |
| defaults: | |
| run: | |
| working-directory: harness/ruview | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 | |
| - name: Set up Node | |
| uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 | |
| with: | |
| node-version: '22' | |
| cache: npm | |
| cache-dependency-path: harness/ruview/package-lock.json | |
| - run: npm ci --ignore-scripts | |
| - run: npm test | |
| - run: npm run test:security | |
| - run: npm run brain:verify | |
| - run: npm run flywheel:verify | |
| - run: npm run manifest:verify | |
| - run: npm pack --dry-run |