feat(nlos): add governed iOS beta setup assistant #9
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/research/consumer-nlos-acceptance-protocol.md' | |
| - 'docs/research/consumer-nlos-beta-test-protocol.md' | |
| - '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/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: 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 | |
| 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 |