Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
74 changes: 74 additions & 0 deletions docs/app-review/check-links.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
#!/usr/bin/env python3
"""Check that every relative link and heading anchor in docs/app-review resolves.

Run from the repository root with: python3 docs/app-review/check-links.py

A link passes when its target file exists and, for links that end in an
#anchor, when the anchor names a heading in the target file, using GitHub's
slug style. Links to websites are skipped. Exits 0 when everything resolves,
prints each broken link and exits 1 otherwise.
"""

import re
import sys
from pathlib import Path

ROOT = Path(__file__).resolve().parent
LINK = re.compile(r"\[[^\]]+\]\(([^)]+)\)")
HEADING = re.compile(r"#{1,6}\s+(.*)")
SKIP_PREFIXES = ("http://", "https://", "mailto:")


def slug(heading_text: str) -> str:
text = heading_text.strip().lower()
text = re.sub(r"[^\w\s-]", "", text)
return re.sub(r"\s", "-", text)


def heading_slugs(path: Path) -> set:
slugs = set()
in_fence = False
for line in path.read_text(encoding="utf-8").splitlines():
if line.startswith("```"):
in_fence = not in_fence
continue
match = HEADING.match(line)
if not in_fence and match:
slugs.add(slug(match.group(1)))
return slugs


def links_outside_fences(path: Path) -> list:
text = path.read_text(encoding="utf-8")
prose = re.sub(r"```.*?```", "", text, flags=re.S)
return LINK.findall(prose)


def main() -> int:
files = {path: heading_slugs(path) for path in sorted(ROOT.rglob("*.md"))}
broken = []
checked = 0
for path, slugs in files.items():
for target in links_outside_fences(path):
if target.startswith(SKIP_PREFIXES):
continue
checked += 1
file_part, _, anchor = target.partition("#")
resolved = (path.parent / file_part).resolve() if file_part else path
if not resolved.is_file():
broken.append(f"{path.relative_to(ROOT)}: {target} (missing file)")
elif anchor and resolved.suffix == ".md":
targets = files.get(resolved) or heading_slugs(resolved)
if anchor not in targets:
broken.append(f"{path.relative_to(ROOT)}: {target} (missing anchor)")
if broken:
print(f"{len(broken)} of {checked} relative links broken:")
for entry in broken:
print(f" {entry}")
return 1
print(f"OK: all {checked} relative links and anchors in docs/app-review resolve")
return 0


if __name__ == "__main__":
sys.exit(main())
2 changes: 1 addition & 1 deletion docs/app-review/product/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,4 +75,4 @@ and Open questions and verification, ending with the commit it was verified agai
| [screens/uploading.md](screens/uploading.md) | Sending the scan and waiting for the result, offline and failure | drafted; checked in the Simulator |
| [screens/result.md](screens/result.md) | The result, its checks and the 3D and AR views | drafted; checked in the Simulator |
| [verification.md](verification.md) | Checks against the running app and their results | current pass at `a39d0a5` |
| [bug-triage.md](bug-triage.md) | Every suspected defect, deduplicated | 16 entries, 2 resolved |
| [bug-triage.md](bug-triage.md) | Every suspected defect, deduplicated | 16 entries, 3 resolved (B-01, B-05, B-15) |
16 changes: 9 additions & 7 deletions docs/app-review/product/bug-triage.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ source and not run. Every blocker and major entry was re-read in the source by a

| ID | Severity | Where the homeowner meets it | Evidence | Decision |
| --- | --- | --- | --- | --- |
| B-03 | Blocker | Camera denied, camera failure, unreadable replay: no message, no way on | seen (replay) | fix |
| B-03 | Blocker | Camera denied or camera failure (find-meter, code only), unreadable replay (onboarding, seen): no message, no way on | seen (replay, on onboarding); camera cases code only | fix |
| B-04 | Blocker | Meter close-up: "Can't get a clear shot" may never appear | code | fix |
| B-09 | Blocker | Feature list: "Add something" does nothing | code | fix |
| B-11 | Blocker | Meter tap can land on another wall's extended plane (ground height fixed at `beede15`) | code | fix |
Expand All @@ -28,12 +28,14 @@ source and not run. Every blocker and major entry was re-read in the source by a

**B-03. Failures other than an unsupported phone never reach the screen.** Only the failure screen
shows a failure, and the app switches to it only for an unsupported phone
(`HouseScan/Runtime/ScanEngine.swift:91-93`). A denied camera, a failed session or an unreadable
replay set the failure without changing screen (lines 105, 379, 381). The homeowner stays on
"Find your electric meter" with no camera and taps do nothing; "Open Settings" is unreachable.
Seen with `-replay /nonexistent`: the app logged "replay unreadable: cannot read
/nonexistent/session.json" and stayed on onboarding (report
`sim/20260926-044618-5520229-5520229d-preview-bad-replay`).
(`HouseScan/Runtime/ScanEngine.swift:91-93`). Two cases, with different evidence:

- **Unreadable replay (seen).** With `-replay /nonexistent` the app logged "replay unreadable:
cannot read /nonexistent/session.json" and stayed on onboarding, with no message and no way on
(report `sim/20260926-044618-5520229-5520229d-preview-bad-replay`).
- **Camera denied, failed session (code, not run).** The source sets the failure without changing
screen (lines 105, 379, 381), so the homeowner stays on "Find your electric meter" with no
camera and taps do nothing; "Open Settings" is unreachable. Read from source only; never run.

**B-04. The close-up's way out can fail to appear.** A failed try counts only when one problem
lasts 4 s with no good frame in between (`HouseScanKit/.../Capture/CloseUpGate.swift`, `evaluate`),
Expand Down
8 changes: 5 additions & 3 deletions docs/app-review/product/foundations/flow.md
Original file line number Diff line number Diff line change
Expand Up @@ -131,9 +131,11 @@ the autopilot wait on each screen until a file named after it appears, for UI te
Scan needs your camera ... Turn on Camera for House Scan in Settings") but only the
`unsupported` screen shows failures, and the app switches to it only when motion tracking is
unsupported (`Runtime/ScanEngine.swift:107`). A denied camera, a failed camera session and an
unreadable replay set the failure (`ScanEngine.swift:119, 489, 491`) without changing screen. The
homeowner would stay on "Find your electric meter" with no camera and no Settings button.
Checkable in the Simulator for the replay case: launch with `-replay /nonexistent`.
unreadable replay set the failure (`ScanEngine.swift:119, 489, 491`) without changing screen. A
denied camera or a failed session leaves the homeowner on "Find your electric meter" with no
camera and no Settings button; that part is read from source and not run. The unreadable replay
is seen: with `-replay /nonexistent` the app stays on onboarding with no message and no way on
([bug-triage.md](../bug-triage.md), B-03).
- "Your scan is saved on this phone" is true only while the app runs: after a relaunch nothing
reopens or retries it.
- Photos of the home accumulate in `Caches/Scans/` across scans and Start over; the app never
Expand Down
4 changes: 3 additions & 1 deletion docs/app-review/product/screens/onboarding.md
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,9 @@ change. Tests find the controls as `action.onboardingNext`, `action.onboardingSk
screen** (triaged as [B-03](../bug-triage.md)). The engine switches to the failure screen only
for an unsupported phone (`Runtime/ScanEngine.swift:105-107`). A denied camera and a failed
session set the failure (`ScanEngine.swift:489, 491`) and an unreadable replay does too
(`ScanEngine.swift:119`), with no screen change. The homeowner who declines the prompt sits on
(`ScanEngine.swift:119`), with no screen change. The unreadable replay is seen: with
`-replay /nonexistent` the app stays on onboarding with no message and no way on; the camera
cases are read from source only. The homeowner who declines the prompt sits on
"Find your electric meter" with no camera image; the "Open Settings" and "Start over" buttons on
`UI/Screens/UnsupportedScreen.swift:35-48` are unreachable outside the `-uiDemo` preview.
- An unsupported phone gets no button: `UnsupportedScreen.swift:44` hides "Start over" for that
Expand Down
4 changes: 2 additions & 2 deletions docs/app-review/ux/friction.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ and for walking past both ends.

| Stage | Trigger | What they see | Way forward | Kind |
| --- | --- | --- | --- | --- |
| Find meter | Camera denied, camera failure, unreadable replay (`Runtime/ScanEngine.swift:119, 489, 491` set the failure without changing screen) | "Find your electric meter" with no camera; taps do nothing | None; close the app | Stuck (B-03) |
| Find meter | Camera denied, camera failure, unreadable replay (`Runtime/ScanEngine.swift:119, 489, 491` set the failure without changing screen) | Camera denied or failed: "Find your electric meter" with no camera, taps do nothing (from source). Unreadable replay: the app stays on onboarding, no message and no way on (seen with `-replay /nonexistent`, B-03) | None; close the app | Stuck (B-03) |
| Find meter | Tap refused: tracking not normal, or no wall under the tap | "Step a little closer to the wall" for both causes | Tap again | Redo |
| Find meter to gap request | Relocalizing for over 20 s (`Runtime/ScanEngine.swift:466-471`) | Back to "Find your electric meter"; wall, marks and photos gone, no explanation | Start the scan again | Redo |
| Close-up | One problem held for 4 s is a failed try; a photo needs 0.6 s of good frames (`Kit/Capture/CloseUpGate.swift:13, 15`) | One instruction and a named fix, "Can't get a clear shot" from the second failed try | Skip; if frames alternate good and blurry, no try fails and the skip never appears | Redo, can stick (B-04) |
Expand All @@ -55,7 +55,7 @@ and for walking past both ends.
The autopilot presses buttons directly. It skips the close-up after 4 s and answers every end "It
turns a corner". It never taps "Add something", "Capture it now" or "Try again", and never
triggers a camera denial or the 20 s reset. Those rows come from the source and were not seen
running.
running, apart from the unreadable replay, seen in a separate report (B-03).

## Three changes, ranked

Expand Down
23 changes: 14 additions & 9 deletions docs/app-review/ux/review.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,11 @@ ARKit, so tracking loss, relocalization and the live meter tap are judged from s
## Verdict: Block

Blockers that fail: R9 (camera access denied leads nowhere, [B-03](../product/bug-triage.md#blockers)),
T5 (the close-up's way out can fail to appear, B-04), R1 (a stretch the homeowner can't reach
is not recorded, B-08) and, from source, I8 (at `a39d0a5` the meter tap can land on another
wall's plane extended past its edge, B-11). The feature list's "Add something" doing nothing
T5 (the close-up's way out can fail to appear, B-04) and, from source, I8 (at `a39d0a5` the meter
tap can land on another wall's plane extended past its edge, B-11). R1 is Not verified: the
checklist judges it on the final visible strip before the upload or finish state, and B-08, the
skipped stretch that is never sent to the server, does not by itself establish that this strip
ends with a silent gap. The feature list's "Add something" doing nothing
(B-09) blocks adding a missed feature. I9, the panel close-up's safety wording, is Not verified
because panel close-ups are not built; with blockers failing, the verdict stays Block.

Expand All @@ -40,12 +42,12 @@ because panel close-ups are not built; with blockers failing, the verdict stays
| `onboarding` | Pass at default size: one message, one primary button, "About 2 min". At AX5 the pill reads "About 2 mi" and the body runs under the page dots. | UX-01 |
| `unsupported` | Pass: "This phone can't measure walls" with what to do instead. Camera denied never reaches this screen. | B-03 |
| `findMeter` | Fails I8 from source: with the meter's wall not yet detected, the tap can land on another wall's plane extended past its edge. Otherwise one instruction, the reticle, "This is my meter". At AX5 the button is pushed partly off screen. Refusal wording ("Step a little closer to the wall") also covers tracking refusals. | UX-01, B-11 |
| `meterCloseUp` | Pass for the meter close-up: one instruction, no shutter, a named fix ("Center the meter in the circle"). The problem pill fails contrast; the way out can fail to appear. I9 Not verified: panel close-ups are not built. | UX-03, B-04 |
| `wallWalk` | One instruction with its reply inside the card, as the checklist asks. The strip's states differ by colour alone, the counter fails contrast, and at AX5 the card covers most of the camera and pushes "Mark something" and the strip off screen. At `525ea40` a marked end is followed by "What's at the left end?", so its kind is truthful, but an end closer than 20 ft still can't be marked. | UX-01, UX-02, UX-03, UX-05, UX-08, B-06 |
| `markFeatures` | Pass at default size: clear list, remove buttons labelled, window question with two full-width answers. At AX5 the list is hidden behind "Looks complete". "Add something" does nothing. | UX-01, B-09 |
| `meterCloseUp` | Pass for the meter close-up: one instruction, no shutter, a named fix ("Center the meter in the circle"). The problem pill failed contrast in the earlier runs and passes at `a39d0a5` (UX-03); the way out can fail to appear. I9 Not verified: panel close-ups are not built. T6 Not verified: the sources document fixes for blur and glare but none for a cut-off label ([meter-close-up.md](../product/screens/meter-close-up.md)), and no run covers one. | UX-03, B-04 |
| `wallWalk` | One instruction with its reply inside the card, as the checklist asks. The strip's states differ by colour alone, the counter fails contrast, and at AX5 the card covers most of the camera and pushes "Mark something" and the strip off screen. At `525ea40` a marked end is followed by "What's at the left end?", so its kind is truthful, but an end closer than 20 ft still can't be marked. From source ([wall-walk.md](../product/screens/wall-walk.md)): guidance toward unobserved ground is the world-anchored ring and dots, not the text I7 asks for, and the target ring pulses while idle, which V4 rules out. A2 Not verified: the strip's spoken summary can read "100 percent seen" while nothing is covered and the ground is unseen, and that description alone does not settle whether tracking status is announced. | UX-01, UX-02, UX-03, UX-05, UX-08, B-06, I7 (from source), V4 (from source) |
| `markFeatures` | Pass at default size: clear list, remove buttons labelled, window question with two full-width answers. At AX5 the list is hidden behind "Looks complete". "Add something" does nothing. The run marked one window; I4's pass holds only for that case (see "Fixed since the preview"). | UX-01, B-09 |
| `gapRequest` | Pass: one line with the reason and target, a progress bar, "I can't get there". The requested stretch reuses the amber that means "seen". | UX-05, UX-06, B-08 |
| `uploading` | Pass: progress in its own steps, apart from coverage (R7); with no server it says "Making a sample result" (`525ea40`). Four text elements do not scale. A failure shows raw error text. | UX-04, B-02 |
| `result` | Pass on honesty: "An installer will take a look", the rules-not-final note, the unseen side disclosed, the borderline window check with measurement, rule and error, and "An installer will check this". A maximum reads like a minimum; the first load leaves the upload screen up for over 1.2 s. | B-14, UX-07 |
| `uploading` | Pass: progress in its own steps, apart from coverage (R7); with no server it says "Making a sample result" (`525ea40`). Earlier runs showed four text elements that do not scale; the audit at `a39d0a5` reports one fixed-size font, the walk's "Mark something" (UX-04). A failure shows raw error text. | UX-04, B-02 |
| `result` | Pass on honesty: "An installer will take a look", the rules-not-final note, the unseen side disclosed, the borderline window check with measurement, rule and error, and "An installer will check this". A maximum reads like a minimum; the first load leaves the upload screen up for over 1.2 s. H5 Not verified: the cited runs end in manual-review and sample results, never a failed result, so what a failed spot then offers is unseen. | B-14, UX-07 |
| `resultAR` | Pass: the spot and cable over the camera with one headline and "Done". The headline says "The spot an installer will check" unless the result is an approved pass, and "Example spot, not your result" for a sample (seen at `525ea40`). A7 Not verified: not run with Reduce Motion on, and the [product description](../product/screens/result.md) reads from source that the AR battery then rises over 0.2 s, where A7 asks for a crossfade or cut. | A7 (from source) |

Checks that passed across the flow: one primary instruction per camera screen (I1); plain words,
Expand Down Expand Up @@ -95,7 +97,10 @@ flagged text on the walk that is drawn rather than exposed to accessibility; the

## Fixed since the preview (`0876e03`)

Seen: the window question answers "It opens" and "It stays shut", stacked (I4), and the result
Seen, with one window marked: the window question answers "It opens" and "It stays shut", stacked
(I4). That one-window run does not establish I4 in general: from source, every window row adds its
own question ([mark-features.md](../product/screens/mark-features.md)), so a scan with several
windows shows one question per row at once, which I4's one-question rule rules out. The result
says the electrical panel still needs an electrician's review (H7). From source: tracking coaching says
"Slow down" and "Aim at a corner or somewhere with more texture" (T1). While relocalizing, the
saved meter photo shows with "Point at the meter like this." (T7). Overlays hide while tracking is
Expand Down