1111 - ' docs/adr/ADR-32[8-9]*'
1212 - ' docs/adr/ADR-33[0-1]*'
1313 - ' docs/adr/ADR-341-consumer-nlos-beta-tester-delivery-and-diagnostics.md'
14+ - ' docs/adr/ADR-342-cognitum-inspired-mobile-instrument-ui.md'
1415 - ' docs/research/consumer-nlos-acceptance-protocol.md'
1516 - ' docs/research/consumer-nlos-beta-test-protocol.md'
17+ - ' docs/screenshots/consumer-nlos-mobile-ui/**'
1618 - ' docs/schemas/ruview-nlos-*.schema.json'
1719 - ' docs/schemas/ruview-ios-visible-depth-diagnostic-v1.schema.json'
1820 - ' docs/security/consumer-nlos-threat-model.md'
2729 - ' ui/mobile/**'
2830 - ' harness/ruview/**'
2931 - ' docs/**consumer-nlos*'
32+ - ' docs/adr/ADR-342-cognitum-inspired-mobile-instrument-ui.md'
33+ - ' docs/screenshots/consumer-nlos-mobile-ui/**'
3034 - ' docs/schemas/ruview-ios-visible-depth-diagnostic-v1.schema.json'
3135 - ' .github/workflows/consumer-nlos-ci.yml'
3236 - ' v2/Cargo.toml'
@@ -118,6 +122,20 @@ jobs:
118122 - run : npm test -- --runInBand
119123 - run : npx tsc --noEmit
120124 - run : npm run lint
125+ - name : Install pinned Playwright Chromium
126+ run : ./node_modules/.bin/playwright install --with-deps chromium
127+ - name : Run production browser E2E and reproduce mobile screenshots
128+ run : npm run e2e:web
129+ - name : Upload browser E2E review evidence
130+ if : ${{ !cancelled() }}
131+ uses : actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
132+ with :
133+ name : consumer-nlos-mobile-ui-browser-${{ github.sha }}
134+ path : |
135+ docs/screenshots/consumer-nlos-mobile-ui
136+ ui/mobile/test-results/playwright
137+ if-no-files-found : warn
138+ retention-days : 14
121139 - name : Export browser bundle
122140 run : npx expo export --platform web
123141 - name : Export Expo iOS bundle
@@ -136,6 +154,142 @@ jobs:
136154 console.log(JSON.stringify(report.metadata?.vulnerabilities ?? {}, null, 2));
137155 JS
138156
157+ mobile-ui-contract :
158+ name : Mobile UI ADR, E2E, and screenshot contract
159+ runs-on : ubuntu-24.04
160+ steps :
161+ - name : Checkout
162+ uses : actions/checkout@11d5960a326750d5838078e36cf38b85af677262
163+
164+ - name : Validate UI-only architecture and deterministic review evidence
165+ shell : bash
166+ run : |
167+ set -euo pipefail
168+ python - <<'PY'
169+ import re
170+ import struct
171+ from pathlib import Path
172+
173+ adr_path = Path(
174+ "docs/adr/ADR-342-cognitum-inspired-mobile-instrument-ui.md"
175+ )
176+ flow_path = Path("ui/mobile/e2e/nlos_mobile_ui.yaml")
177+ screenshot_dir = Path("docs/screenshots/consumer-nlos-mobile-ui")
178+
179+ adr = adr_path.read_text(encoding="utf-8")
180+ required_sections = (
181+ "## Context",
182+ "## Specification",
183+ "## Pseudocode and state transitions",
184+ "## Architecture",
185+ "## Decision",
186+ "## Alternatives with quantified tradeoffs",
187+ "## Accessibility",
188+ "## Security and privacy",
189+ "## Performance budgets",
190+ "## Refinement and failure handling",
191+ "## Requirement to evidence mapping",
192+ "## Rollout and rollback",
193+ "## Acceptance test",
194+ )
195+ for section in required_sections:
196+ assert section in adr, f"missing ADR section: {section}"
197+
198+ required_adr_contract = (
199+ "UI-only",
200+ "feat/consumer-nlos-ruview",
201+ "Cognitum Explain",
202+ "No Cognitum name, logo, wordmark",
203+ "SYNTHETIC",
204+ "LIVE VERIFIED",
205+ "LIVE UNVERIFIED",
206+ "STALE",
207+ "DISCONNECTED",
208+ "390 by 844",
209+ "44 by 44 points",
210+ "under 1.5 seconds",
211+ "under 100 milliseconds",
212+ "zero runtime dependency",
213+ "physical iPhone validation pending",
214+ )
215+ for phrase in required_adr_contract:
216+ assert phrase in adr, f"missing mobile UI contract: {phrase}"
217+
218+ flow = flow_path.read_text(encoding="utf-8")
219+ required_flow_contract = (
220+ "appId:",
221+ "---",
222+ "launchApp:",
223+ "tapOn:",
224+ "assertVisible:",
225+ "nlos-evidence-state",
226+ "nlos-synthetic-watermark",
227+ "nlos-beta-setup",
228+ )
229+ for token in required_flow_contract:
230+ assert token in flow, f"missing Maestro UI contract token: {token}"
231+
232+ screen_sources = "\n".join(
233+ path.read_text(encoding="utf-8")
234+ for path in sorted(Path("ui/mobile/src/screens/NLOSScreen").glob("*.tsx"))
235+ )
236+ for test_id in (
237+ "nlos-evidence-state",
238+ "nlos-synthetic-watermark",
239+ "nlos-beta-setup",
240+ ):
241+ assert test_id in screen_sources, f"missing screen contract: {test_id}"
242+ for state in (
243+ "SYNTHETIC",
244+ "LIVE VERIFIED",
245+ "LIVE UNVERIFIED",
246+ "STALE",
247+ "DISCONNECTED",
248+ ):
249+ assert state in screen_sources, f"missing evidence state: {state}"
250+
251+ state_type = re.search(
252+ r"export type NlosEvidenceState\s*=\s*(.*?);",
253+ screen_sources,
254+ re.DOTALL,
255+ )
256+ assert state_type, "missing NlosEvidenceState type contract"
257+ actual_states = set(re.findall(r"'([^']+)'", state_type.group(1)))
258+ expected_states = {
259+ "SYNTHETIC",
260+ "LIVE VERIFIED",
261+ "LIVE UNVERIFIED",
262+ "STALE",
263+ "DISCONNECTED",
264+ }
265+ assert actual_states == expected_states, (
266+ f"evidence state taxonomy drift: {sorted(actual_states)}"
267+ )
268+
269+ def png_dimensions(path):
270+ data = path.read_bytes()
271+ assert data.startswith(b"\x89PNG\r\n\x1a\n"), f"not a PNG: {path}"
272+ assert data[12:16] == b"IHDR", f"missing PNG IHDR: {path}"
273+ return struct.unpack(">II", data[16:24])
274+
275+ expected_screenshots = (
276+ "overview-390x844.png",
277+ "synthetic-390x844.png",
278+ "setup-390x844.png",
279+ )
280+ for name in expected_screenshots:
281+ path = screenshot_dir / name
282+ assert path.is_file(), f"missing screenshot baseline: {path}"
283+ assert png_dimensions(path) == (390, 844), (
284+ f"wrong screenshot dimensions for {path}: {png_dimensions(path)}"
285+ )
286+
287+ print(
288+ "Validated ADR 342, Maestro UI contract, five evidence states, "
289+ "and three 390x844 PNG baselines"
290+ )
291+ PY
292+
139293 native-ios :
140294 name : Native Swift, simulator, and unsigned archive dry gate
141295 runs-on : macos-26
0 commit comments