Skip to content

feat(mobile): refine spatial target UI #15

feat(mobile): refine spatial target UI

feat(mobile): refine spatial target UI #15

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