1010 - ' harness/ruview/**'
1111 - ' docs/adr/ADR-32[8-9]*'
1212 - ' docs/adr/ADR-33[0-1]*'
13+ - ' docs/adr/ADR-341-consumer-nlos-beta-tester-delivery-and-diagnostics.md'
1314 - ' docs/research/consumer-nlos-acceptance-protocol.md'
15+ - ' docs/research/consumer-nlos-beta-test-protocol.md'
1416 - ' docs/schemas/ruview-nlos-*.schema.json'
17+ - ' docs/schemas/ruview-ios-visible-depth-diagnostic-v1.schema.json'
1518 - ' docs/security/consumer-nlos-threat-model.md'
1619 - ' .github/workflows/consumer-nlos-ci.yml'
1720 - ' v2/Cargo.toml'
2427 - ' ui/mobile/**'
2528 - ' harness/ruview/**'
2629 - ' docs/**consumer-nlos*'
30+ - ' docs/schemas/ruview-ios-visible-depth-diagnostic-v1.schema.json'
2731 - ' .github/workflows/consumer-nlos-ci.yml'
2832 - ' v2/Cargo.toml'
2933 - ' v2/Cargo.lock'
@@ -133,17 +137,25 @@ jobs:
133137 JS
134138
135139 native-ios :
136- name : Native Swift and iOS Simulator build
137- runs-on : macos-15
140+ name : Native Swift, simulator, and unsigned archive dry gate
141+ runs-on : macos-26
138142 defaults :
139143 run :
140144 working-directory : ui/ios-nlos
141145 steps :
142146 - name : Checkout
143147 uses : actions/checkout@11d5960a326750d5838078e36cf38b85af677262
144148
149+ - name : Require Xcode 26
150+ shell : bash
151+ run : |
152+ set -euo pipefail
153+ xcodebuild -version
154+ xcode_major="$(xcodebuild -version | awk '/^Xcode / {split($2, version, "."); print version[1]}')"
155+ test "$xcode_major" = "26"
156+
145157 - name : Swift protocol and security tests
146- run : swift test -Xswiftc -strict-concurrency=complete -Xswiftc -warnings-as-errors
158+ run : swift test
147159
148160 - name : Unsigned iOS Simulator build
149161 run : >-
@@ -158,6 +170,154 @@ jobs:
158170 SWIFT_TREAT_WARNINGS_AS_ERRORS=YES
159171 build
160172
173+ - name : Unsigned generic iOS archive dry gate
174+ shell : bash
175+ run : |
176+ set -euo pipefail
177+ archive_path="$RUNNER_TEMP/RuViewNLOS.xcarchive"
178+ xcodebuild \
179+ -project RuViewNLOS.xcodeproj \
180+ -scheme RuViewNLOS \
181+ -configuration Release \
182+ -sdk iphoneos \
183+ -destination 'generic/platform=iOS' \
184+ -archivePath "$archive_path" \
185+ CODE_SIGNING_ALLOWED=NO \
186+ CODE_SIGNING_REQUIRED=NO \
187+ SWIFT_STRICT_CONCURRENCY=complete \
188+ SWIFT_SUPPRESS_WARNINGS=NO \
189+ SWIFT_TREAT_WARNINGS_AS_ERRORS=YES \
190+ archive
191+ test -f "$archive_path/Info.plist"
192+ if find "$archive_path" -name embedded.mobileprovision -print -quit | grep -q .; then
193+ echo "Unsigned dry archive unexpectedly contains a provisioning profile" >&2
194+ exit 1
195+ fi
196+
197+ release-governance :
198+ name : Beta release governance contract
199+ runs-on : ubuntu-24.04
200+ steps :
201+ - name : Checkout
202+ uses : actions/checkout@11d5960a326750d5838078e36cf38b85af677262
203+
204+ - name : Validate ADR, protocol, workflow, and diagnostic boundary
205+ shell : bash
206+ run : |
207+ set -euo pipefail
208+ python - <<'PY'
209+ import json
210+ from pathlib import Path
211+
212+ adr_path = Path("docs/adr/ADR-341-consumer-nlos-beta-tester-delivery-and-diagnostics.md")
213+ protocol_path = Path("docs/research/consumer-nlos-beta-test-protocol.md")
214+ workflow_path = Path(".github/workflows/consumer-nlos-ci.yml")
215+ schema_path = Path("docs/schemas/ruview-ios-visible-depth-diagnostic-v1.schema.json")
216+
217+ adr = adr_path.read_text(encoding="utf-8")
218+ protocol = protocol_path.read_text(encoding="utf-8")
219+ workflow = workflow_path.read_text(encoding="utf-8")
220+ schema = json.loads(schema_path.read_text(encoding="utf-8"))
221+
222+ required_adr_sections = (
223+ "## Specification",
224+ "## Pseudocode and state transitions",
225+ "## Architecture",
226+ "## Release architecture",
227+ "## Alternatives considered",
228+ "## Refinement and failure handling",
229+ "## Requirement-to-evidence mapping",
230+ "## Acceptance test",
231+ )
232+ for section in required_adr_sections:
233+ assert section in adr, f"missing ADR section: {section}"
234+
235+ required_contract = (
236+ "under five minutes",
237+ "15 second calibration",
238+ "30 second wall scan",
239+ "65,536 bytes",
240+ "blocked_raw_transients_unavailable",
241+ "No raw capture",
242+ )
243+ combined = adr + "\n" + protocol
244+ for phrase in required_contract:
245+ assert phrase in combined, f"missing beta contract: {phrase}"
246+
247+ assert "runs-on: macos-26" in workflow
248+ assert 'test "$xcode_major" = "26"' in workflow
249+ assert "CODE_SIGNING_ALLOWED=NO" in workflow
250+ assert "CODE_SIGNING_REQUIRED=NO" in workflow
251+ assert ("sec" + "rets.") not in workflow
252+ assert ("upload" + "-app-store") not in workflow.lower()
253+ assert ("al" + "tool") not in workflow.lower()
254+
255+ phase = {
256+ "phase": "calibration",
257+ "plannedDurationSeconds": 15,
258+ "observedDurationSeconds": 15.0,
259+ "frameCount": 450,
260+ "averageFPS": 30.0,
261+ "averageDepthCoverage": 0.91,
262+ "averageMovementMetersPerSecond": 0.08,
263+ "finalTrackingState": "normal",
264+ "peakThermalState": "nominal",
265+ }
266+ diagnostic = {
267+ "schema": "ruview.ios.visible-depth-diagnostic.v1",
268+ "sessionId": "00000000-0000-4000-8000-000000000000",
269+ "createdAt": "2026-08-23T00:00:00Z",
270+ "deviceModelFamily": "iPhone",
271+ "osVersion": "iOS 26.0",
272+ "appVersion": "1.0 (1)",
273+ "capabilities": {
274+ "worldTracking": True,
275+ "sceneDepth": True,
276+ "smoothedSceneDepth": True,
277+ "sceneMesh": True,
278+ "rawPhotonHistograms": False,
279+ },
280+ "phases": [phase, {**phase, "phase": "wall_scan", "plannedDurationSeconds": 30}],
281+ "consent": {
282+ "localValidation": True,
283+ "diagnosticExport": True,
284+ "rawSensorExport": False,
285+ },
286+ "evidenceLabel": "direct_depth",
287+ "physicalNLOSStatus": "blocked_raw_transients_unavailable",
288+ "cameraPermission": "granted",
289+ "completionStatus": "completed",
290+ }
291+ encoded = json.dumps(
292+ diagnostic, sort_keys=True, separators=(",", ":"), allow_nan=False
293+ ).encode("utf-8")
294+ assert len(encoded) <= 65_536
295+ assert set(diagnostic) == set(schema["required"])
296+ assert diagnostic["consent"]["rawSensorExport"] is False
297+ assert diagnostic["capabilities"]["rawPhotonHistograms"] is False
298+ assert diagnostic["physicalNLOSStatus"] == (
299+ "blocked_raw_transients_unavailable"
300+ )
301+
302+ forbidden = {
303+ "image", "images", "rgb", "depth", "depth_map", "point_cloud",
304+ "csi", "transient", "histogram", "token", "authorization",
305+ "latitude", "longitude", "location", "trajectory", "raw",
306+ }
307+
308+ def walk(value):
309+ if isinstance(value, dict):
310+ for key, child in value.items():
311+ assert key.lower() not in forbidden, f"forbidden diagnostic key: {key}"
312+ walk(child)
313+ elif isinstance(value, list):
314+ for child in value:
315+ walk(child)
316+
317+ walk(diagnostic)
318+ print(f"Validated beta governance contract and {len(encoded)} byte fixture")
319+ PY
320+
161321 metaharness :
162322 name : Advisory MetaHarness
163323 runs-on : ubuntu-latest
0 commit comments