diff --git a/experiments/device-field-test/build-4.1/README.md b/experiments/device-field-test/build-4.1/README.md new file mode 100644 index 000000000..b5cd1d7f6 --- /dev/null +++ b/experiments/device-field-test/build-4.1/README.md @@ -0,0 +1,13 @@ +# Build 4.1 field test: three analysed runs + +TestFlight build 4.1 (`beta` at `45017ba`) was recorded three times on 2026-09-26, and each recording was analysed in its +own session. Each report lists issues with stable IDs so they can be compared and deduplicated before filing. + +| Report | Recording | Site | Phone | IDs | +|---|---|---|---|---| +| [run1-2000.md](run1-2000.md) | 20:00, 6 min 49 s | Mock wall, indoors | iPhone 14, no LiDAR | `A41-1-…` | +| [run2-2015.md](run2-2015.md) | 20:15 | Mock wall, indoors | iPhone 14, no LiDAR | `A41-2015-…` | +| [run3-2055.md](run3-2055.md) | 20:55 | A real house, outdoors at night | iPhone 14, no LiDAR | `A41-2055-…` | + +These are commit-safe copies: meter numbers are removed. Videos, frames, transcripts, scan zips and server re-runs stay +in the gitignored `private/device-test-4.1-*/` folders on the analysing machine. Run 3's media shows a real address. diff --git a/experiments/device-field-test/build-4.1/run1-2000.md b/experiments/device-field-test/build-4.1/run1-2000.md new file mode 100644 index 000000000..60ead2559 --- /dev/null +++ b/experiments/device-field-test/build-4.1/run1-2000.md @@ -0,0 +1,538 @@ +# Device test, build 4.1, run 1 of 3: the 20:00 recording of 2026-09-26 + +Commit-safe copy: the meter serial is removed. Frames, transcript, scan zip and server output stay local in `private/device-test-4.1-2000/` (gitignored). + +This is thread 1 of 3 analysing TestFlight 4.1. A later session compares all three. The **issue list** is +the next section and uses stable IDs (`A41-1-…`) so the comparison can match findings across threads. + +## Inputs and how they were analysed + +| Item | Value | +|---|---| +| Video | `Downloads\20_00 9_26_2026.MP4`: 6 min 49 s, 1170×2532 at 60 fps, microphone on. Status-bar clock 19:52 → 19:58 | +| Scan | `Downloads\20_00 9_26_2026.zip`, unzipped to `scan/`: `manifest.json`, `scene.json`, 206 photos, 6 sensor streams. **It is the same run as the video**: the 17 guidance entries line up with the video at a constant offset (video time = manifest time since the close-up + 25.0 s) | +| Build | HouseScan 0.1.0 (4.1), from `beta` `45017ba` (ios-mvf `d02cc11` + #34 #37 #47 #48 #51, server #50). Code read in a detached worktree at `C:\dev\house-scanning-beta41` | +| Phone | **iPhone 14 (`iPhone14,7`), no LiDAR**, iOS 26.6.2. `scene_depth_enabled: false`, `mesh_enabled: false`. Runs 1–2 of the field test used a LiDAR phone (inferred from those reports), so **this is the first non-LiDAR run** | +| Site | The same mock wall as runs 1–2: meter, disconnect, "The Grid" panel box, window (~7 ft right), an **existing Base battery** (~11 ft right), a folding table as the AC stand-in (~17 ft right), wooden posts at both ends, turf, eave. Indoors, in an office | + +Working files here: `frames1s/` (1 fps), `sheets/` (12-up contact sheets with timestamps), `hi/` (full-res +evidence frames, `ev_*.jpg` are the key sheets), `transcript.txt` / `.json` (faster-whisper `medium`, word +timestamps), `server_rerun.json`, `siteplan.svg`, `coverage_replay.txt`. + +Checks run: +- `python -m packet validate scan` passes: packet 1.1, 206 photos at 1920×1440, 0 depth, no mesh, 22 planes, + 5 marks, 17 guidance entries, 347.7 s, 73.3 m walked. +- The server solver at `beta` was re-run on `scan/scene.json` with the public demo rules. It reproduces the + phone's result exactly: `manual_review`, spot 12 ft 3 in right, 13 ft of cable. +- `coverage_replay.py` was run on the scene (see A41-1-03). + +## Headline + +1. **The app used a table top as the ground.** `groundBelow` takes the *highest* horizontal plane at least + 0.3 m below the meter, with no check on its classification. It picked an ARKit plane classified `table`, + 0.51 m below the meter, over the detected `floor` plane 1.47 m below it. So **every height in the scan is + about 3 ft 2 in too high above the real ground.** + - The meter is exported at 1 ft 8 in up. The real height is about 4 ft 10 in. + - The window's bottom is exported at 0 ft 2 in. The real height is about 3 ft 4 in. + - The ground targets are drawn on the wall. The gap patches are drawn at exactly the folding table's top + (frames 4:44–4:47, `hi/ev_patch_table_height.jpg`). + - The tilt-down task at the meter could never finish. + + The tester noticed at 3:13: *"I think the ground is a little too high."* This one bug causes most of the + run's other problems. +2. **"See it on your wall" showed nothing.** For 15 s (6:10–6:25) the camera swept the wall, including the + 11–13 ft stretch, the meter and the conduit. No battery, cable or tint appeared, while the card read "The + spot an installer will check". Tester: *"See it on your wall. Where is it going? I don't know where it + even is."* This is a **regression** from #41's move to RealityKit (`2d33cea`). Run 2 on build 3.1 did draw + the battery. +3. **The walk was dominated by one prompt that could never be met.** "Tilt down to show the ground / The strip + along the wall, at your meter" was on screen for about **151 s of the 225 s walk**. It came back 3 s after + it was met once. It was still asked while the tester stood 15 ft away. The app never asked the tester to + walk right; they did it on their own. +4. **The new corner flow trapped the tester twice**, for 26 s and 16 s. It offers "It turns a corner" or + "Something blocks it", but nothing for "the wall just ends". At 1:44 "Mark next wall" accepted a surface + behind the post. +5. **The result is `manual_review`**, spot 12 ft 3 in right of the meter (10 ft 11 in → 13 ft 6 in), 13 ft + of cable. It still can't be a pass: + - the ground was never covered; + - the server's drift model makes everything more than a few feet from the meter "too close to call" (AC + ± 5 ft 7 in, cable 12 ft 7 in ± 6 ft 5 in); + - the spot appears to sit where the **existing Base battery** stands (inferred; #27). +6. **UX verdict: worst run so far for the tester's experience.** From 4:35 on the narration is a plea: + *"Claude, please tell me this is awful. This experience is terrible. What do you want from me? … I have no + idea where you want me to point."* The main unexplained element was **"the blue circle"**, meaning the + target ring and the arrow chevrons (7 mentions). + +**What improved since 3.1:** +- **#24 looks fixed.** "Can't get there" now ends the wall where the phone walked: left end 1 ft 0 in left, + right end 20 ft 3 in right. Before, it put the end at the meter. +- **#29 looks fixed.** Automatic "What's at the left/right end?" questions appeared at both physical ends. +- **#25 is fixed.** The mark prompt now reads "Tap the AC unit". +- The meter tap refusal ("Step a little closer to the wall", F-2), the close-up and the meter-number pick all + worked. + +## Issue list + +**Sev:** P0 = breaks the run or the result; P1 = major usability or correctness; P2 = moderate; P3 = polish. +**Lane:** app = iOS capture (Sam + agents); server = `server/` (Hunter + agents); test = test process. + +| ID | Sev | Lane | Title | Existing issue | Evidence (video time) | +|---|---|---|---|---|---| +| A41-1-01 | P0 | app | Ground height taken from a `table` plane: every height is ~3 ft 2 in too high | new | 3:13 narration; 3:44–3:46; 4:44–4:47; 5:58; packet planes | +| A41-1-02 | P0 | app | AR "See it on your wall" draws nothing (regression from #41's RealityKit path) | new (regression of #41) | 6:10–6:25; 6:05 narration | +| A41-1-03 | P1 | app | "Tilt down… at your meter" can't finish, blocks the walk and never escalates (151 s of 225 s) | #28 inverted | 0:33–4:13 | +| A41-1-04 | P1 | app | Ground task re-asked 3 s after it was met: a ground re-estimate rebuilds coverage | new | 0:52–0:58 (`hi/ev_ground_reset.jpg`) | +| A41-1-05 | P1 | app | The target ring and arrow chevrons are unexplained, show no progress, and contradict the text | new | 0:41, 0:53, 0:58, 3:44–3:46, 5:47 | +| A41-1-06 | P1 | app | Corner flow: no "wall just ends" answer; the "Mark next wall" refusals loop; a surface behind the post was accepted as the next wall | new | 1:14–1:44, 2:02–2:20, 2:47–2:55 | +| A41-1-07 | P1 | app | Phone gap "Show the ground about 11 ft right" stuck at 0% for ~55 s, 8% at most | new (cause A41-1-01) | 4:29–5:39 | +| A41-1-08 | P1 | server+app | Request "Show the ground out to about 15 ft from the wall. Around your meter" can't be met, and its copy is wrong | new | 5:44–6:05 | +| A41-1-09 | P1 | server | Drift of 0.16 ft/ft makes any spot more than a few feet from the meter unresolvable, so the result is always `manual_review` | new | result 6:06, `server_rerun.json` | +| A41-1-10 | P2 | app+server | The spot appears to overlap the existing Base battery; there's no way to mark it | #27 | 4:33–5:35, result | +| A41-1-11 | P2 | app | "A closer spot may exist on the left… The scan didn't reach that side" although the physical left end was reached | new (follows from 06) | 6:09 | +| A41-1-12 | P2 | app | "6 more views to finish", then one view and straight to the result | #39 (still present) | 5:43–6:06 | +| A41-1-13 | P2 | app | "Slow down" while tilting or standing | #26 (still present) | 3:49, 4:08, 5:07, 5:48, 5:58, 6:04 | +| A41-1-14 | P2 | app | "Can't get there" on a tilt task: the label is wrong (the tester *can* get there; the phone can't see it) | new | the whole walk | +| A41-1-15 | P2 | app | "Does this window open?" can be left unanswered; exported without `operable` | new | 4:26–4:28 | +| A41-1-16 | P3 | app | "Ending the wall here leaves out 2 ft you walked" shows under the strip during a tilt task, unprompted | new | 1:49–2:00 | +| A41-1-17 | P3 | app | The upload skips the "Check clearances" tick on the final upload | #31 (partly) | 6:05 | +| A41-1-18 | P3 | app | The gap patch is drawn through the existing battery (no occlusion or awareness of objects) | related #41 | 4:33, 5:24–5:35 | +| A41-1-19 | P3 | test | Notification banners covered the app during recording; turn on a Focus mode | – | 3:05–3:08, 5:17–5:22 | +| A41-1-20 | P3 | app | The share zip is 119.5 MB and takes a long time to upload | new | 6:31–6:48 | + +### A41-1-01 (P0, app): ground height taken from a `table` plane + +**What happened.** +- `session.meter_anchor.ground_y_m` is −0.5146 m. +- The packet's `planes` include a `floor` plane at y = −1.474 m (11.2 m × 8.0 m) and a `table` plane at + y = −0.516 m, 1.4 m out from the wall, with a 1.0 m × 5.9 m extent. +- `ground_y_m` equals the table plane's height to the millimetre. + +**Effects** (1 m ≈ 3 ft 3 in; the offset is 0.96 m = 3 ft 2 in): +- `scene.meter.pos` y = 1.69 ft, so the meter is exported 1 ft 8 in above "ground". The real height is about + 4 ft 10 in. +- The window is exported at 0.19–4.06 ft. The real height is about 3.3–7.2 ft. +- Every ground sample row (0, 2 and 4 ft out) sits in mid-air, 3 ft above the turf. The foot-of-wall row is a + point **on the wall** below the meter. That's why the target ring sits on the wall and the post + (3:24–4:11, `hi/ev_tilt_arrow.jpg` frame 4). +- The arrow says **tilt up** while the card says "Tilt down" (3:44–3:46): the target is above the camera's + view of the turf. +- The ground coverage row never goes green. The strip's ground row stayed amber for the whole walk. + `scene.coverage.observed` has ground only at 0–1 ft and 10–11.5 ft. +- The gap patches ("about 11 ft right", "15 ft from the wall") are drawn at the folding table's height; at + 4:44–4:47 the patch lies exactly on the tabletop. At 5:58–5:59 the patch at the meter is drawn at the + bottom of the conduit, not on the turf. +- The AR battery, had it rendered, would stand on that plane, about 3 ft above the turf (inferred from + `ResultARModel.build`, which puts the unit at height 0 = `groundY`). +- Server heights are all shifted: the headroom and "box above" bands (6.5 ft up) and window heights for the + opening check. The overhead tilt target (`tiltUpAbove`) is too low by 3 ft. + +**Root cause.** +- `groundBelow` (`ios/HouseScan/Runtime/ScanEngine+Actions.swift:57-66`) returns + `near.map(\.y).max()`, the **highest** horizontal plane that is ≥ 0.3 m below the meter and within 2 m + (plane centre distance minus its half-diagonal). +- `LiveCapture` passes every horizontal `ARPlaneAnchor` with no `classification` filter + (`ios/HouseScan/Runtime/LiveCapture.swift:406-413`). +- A 5.9 m-long plane has a 3 m "radius", so the 2 m test barely constrains it. +- `refineGround` (`ScanEngine.swift:364-379`) re-runs this whenever the plane set changes, and lets a + *higher* plane replace a lower one ("a better plane replaces an earlier one"). + +**Fix direction.** +- Take only `.floor` planes, or `.none` where classification isn't supported; drop `.table`, `.seat`, + `.ceiling` and so on. +- Prefer the **lowest** plausible plane, or the plane under the phone's own position. +- Never let a later plane *raise* the ground by more than ~10 cm once a floor exists. +- Add a sanity band for meter height: 3–6.5 ft above the ground. Outside it, keep the guess and set + `groundMeasured = false`. +- Log the ground's history in the packet: its source plane id and classification. + +**Also note.** With LiDAR, runs 1–2 probably had depth or mesh ground and a detected floor early (inferred). +Without LiDAR, planes arrive late and a table can come first. This is the non-LiDAR path's first real test. + +### A41-1-02 (P0, app): AR view draws nothing + +**What happened.** +- 6:09 "See it on your wall", then 6:10–6:25 the live camera with "The spot an installer will check / 12 ft 3 in + right of your meter, 13 ft of cable". +- Nothing is drawn in any frame at 2 fps (`hi/ar_sheet.jpg`, `hi/ev_ar_empty.jpg`). That includes frames + aimed at the meter and conduit, where the cable should start, and at the 10–14 ft stretch. +- The card shows the spot's text, not "Point at your meter". So `state.tracking == .normal` and the result + has a spot (`ResultARScreen.swift:37-48`). + +**Code path (new in 4.1, #41 / `2d33cea`).** +- `ScanEngine.showResultInCamera` builds `ResultARModel` and calls `LiveCapture.showResult`, which hangs it + on an `AnchorEntity(.anchor(identifier:))` for the meter anchor, with + `model.orientation = simd_quatf(anchor.transform).inverse`. It then turns on occlusion. +- On an iPhone 14 that means re-running the session with `.personSegmentationWithDepth` + (`LiveCapture.swift:130-170`). +- The SwiftUI Canvas fallback draws only when `!state.resultInCamera` (`ResultARScreen.swift:17`), so once + `showResult` returns true there's no fallback. + +**Hypotheses, for whoever debugs on a device.** Nothing below is confirmed. +- (a) Re-running the session for people occlusion drops or re-identifies the meter anchor, so the + `AnchorEntity` never resolves. +- (b) The entity is enabled but placed off-screen: a wrong orientation or position composition, or the + anchor's transform is not the frame `wall.meter` uses. +- (c) `setResultVisible(false)` was applied during a brief non-normal tracking state caused by the session + re-run, and never re-enabled. `ingest` re-enables only when `frame.tracking` changes, and in `.idle` mode + frames may not be delivered. +- (d) People occlusion hides everything: the whole view segmented as a person or with depth in front. + +**Fix direction.** +- Check `RuntimeLog` and whether `showResult` returned true. +- Keep the Canvas overlay as a fallback until the RealityKit entity is confirmed on screen. +- Don't re-run the session on the AR screen; enable people occlusion from the start. +- Add a UI test hook. +- Independently of this, A41-1-01 would put the battery 3 ft up. + +### A41-1-03 (P1, app): the ground task at the meter blocks the walk + +The guidance log: + +| Entry | Video time | Duration | Outcome | +|---|---|---|---| +| g3 | 0:33–0:54 | 21 s | met | +| g5 | 0:57–1:18 | 21 s | superseded | +| g8 | 1:47–2:04 | 16 s | superseded | +| g10 | 2:20–2:48 | 28 s | superseded | +| g13 | 3:05–3:40 | 34 s | superseded | +| g15 | 3:42–4:13 | 31 s | superseded | + +That's **≈151 s of "Tilt down… at your meter"**. The span is always ±0.30 m around the meter. + +- During 2:21–2:46 the tester walked to the right end (20 ft) while the card still asked for the ground "at + your meter", with a left chevron pointing back. +- "Walk slowly to your right" was **never shown** in the whole run. +- The coverage replay agrees: the foot-of-wall ground row (0 ft out) in the cells either side of the meter was + seen by only k00004–k00006 and k00125. The reach never exceeds 1 ft. + +Narration: +- *"I don't know what it wants me to see"* (0:41) +- *"That's the ground and that's the ground. I don't know what it's looking at"* (2:27) +- *"What the heck does it want? I have no clue"* (3:46) +- *"I'm like putting the thing in the center and I don't know what's going on. Claude, help."* (4:01) + +**Root cause.** A41-1-01 makes the task impossible. The planner also has no give: +- no timeout; +- no auto-skip after N seconds; +- no "move on and come back"; +- the ground task at the meter outranks walking. + +**Fix.** +- Fix A41-1-01. +- Auto-skip a tilt task after ~20 s of the target in view without progress (mark it for review), or offer + "Skip this". +- Never let one ±1 ft task block the walk for more than ~30 s. +- Don't say "at your meter" when the phone is 15 ft away; say "go back to your meter" or let the walk + continue. + +Relation to #28: run 1 never asked for the ground at the meter; now it asks only for that. + +### A41-1-04 (P1, app): the met ground task comes back + +At 0:52.5–0:54.5 the strip's ground row near the meter turned green, and g3 was logged `met` at 0:54. At 0:57 +the cells were amber again and g5 re-asked the same ±0.30 m span (`hi/ev_ground_reset.jpg`). + +The most likely cause: `refineGround` (`ScanEngine.swift:364-379`) picked up a new, higher plane (the table) +and replaced `groundY`. `coverage.updateWall` rebuilt coverage at the new row heights, and the photos no +longer covered them. This is inferred: planes have no timestamps in the packet. Whatever the fix for +A41-1-01, log each ground change with its time, and don't silently un-cover what the user was just told was +done. + +### A41-1-05 (P1, app): "the blue circle" is unexplained + +Two different blue things confused the tester: +- the large target ring with a centre dot (the aim target for the task); +- the small filled circles with a chevron at the screen edge (the direction to the target). + +The ring shows no progress: it doesn't fill, change colour or tick while the tester holds it on target. A +separate check-mark bubble appeared briefly (0:52, 0:58, 1:52, 5:25) with no explanation. + +Narration: +- *"I wonder if we could have like a loading thing where the circle goes green once it's captured"* (0:47) +- *"I'm not sure what this blue circle on the ground means"* (0:53) +- *"What does this blue circle want from me?"* (1:00) +- *"What does this blue circle mean? What does it want?"* (5:47) + +The chevron pointed **up** during "Tilt down" (3:44–3:46; the cause is A41-1-01). Onboarding explains the +haze but not the ring or the arrows. + +**Fix.** +- Fill or turn the ring green as views accumulate, with a tick when the cell is covered. +- Add a one-line legend on the first appearance ("Put the circle on the ring and hold still"). +- Make sure the arrow never contradicts the text. + +### A41-1-06 (P1, app): the corner flow + +**Left end:** +- 1:14: automatic "What's at the left end? / This tells the installer whether the wall keeps going", with "It + turns a corner" and "Something blocks it". +- The tester tapped **"It turns a corner"** (1:17). Then 26 s of: "Walk round the corner, then aim at the next + wall / Mark next wall", "No wall under the circle / Step closer and aim at the next wall", "You're past the + end of the wall / Photos here aren't kept", "That wall doesn't meet this one at the corner". +- At **1:43.8 "Mark next wall" was accepted** (g6 `met`; the strip reopened on the left) while the phone + aimed at the plywood face behind the post (`hi/ev_nextwall_office.jpg`). +- At 2:02 the same question came again, "It turns a corner" again, 16 s more refusals, then **"Can't get + there" at 2:20**. The left end went unexplored at −1.0 ft. +- The accepted corner doesn't survive into `scene.json`: one wall, both ends `unexplored`. It was presumably + clipped by the later end. +- Narration: *"No wall under the circle, you're past the end of the wall. What? … I don't understand what it + wants"* (1:24–1:31); *"What is it talking about? Okay, well, can't get there"* (2:03–2:21). + +**Right end:** +- 2:47: automatic "What's at the right end?", then "It turns a corner" again. +- "No wall under the circle", "doesn't meet this one at the corner", then "Can't get there" at 2:55. The right + end went unexplored at 20.25 ft. + +**Problems:** +- There's no answer for "the wall just ends / free-standing / fence ends". The mock wall is free-standing, but + real garden walls, fences and bump-outs end too. "Something blocks it" doesn't read as "it ends". +- A wrongly chosen "It turns a corner" has no back or undo except the ambiguous "Can't get there". +- `CoverageMap.maxCornerFromEnd` = 3 m (`HouseScanKit/.../CoverageMap.swift:1124`) is generous enough to + accept an unrelated surface near the end. +- "You're past the end of the wall" coaching fights the "walk round the corner" instruction: walking round the + corner *is* going past the end. + +**Fix.** +- Add a third answer, "The wall ends here", which sets a `limit` end. +- Add "Back" on the next-wall step. +- Suppress past-end coaching while `nextWallSide != nil`. +- Tighten the corner bound, or require the new wall's line to pass within ~1 m of the marked end. + +### A41-1-07 (P1, app): phone gap "about 11 ft right" never fills + +After "Looks complete" (4:28) the phone raised its own gap (g16, span 0.30–6.10 m): "Show the ground about 11 +ft right of your meter / This might be a spot for the battery, so the ground there needs a clear look from two +places". +- The progress bar read **0% from 4:29 to about 5:24**, then 8% until "I can't get there" at 5:39. +- The patch was drawn at table height (A41-1-01) and through the existing battery (A41-1-18). +- *"What does this mean? What does it want? … Where do you want me to point?"* (4:35–5:05). + +Besides the root cause: there's no timeout, and "a clear look from two places" is unclear. A "step to a second +spot" cue would help. The gap loop also started *after* "Looks complete", so the user thought they had +finished. + +### A41-1-08 (P1, server + app): the "15 ft from the wall" request + +Server request g17: "6 more views to finish / Show the ground out to about 15 ft from the wall / Around your +meter. Step back and tilt down until that much ground is in view". +- The band is `ground`, span −1.0 → 20.25 ft (the whole wall), 14.3 ft out: the pool check's 10 ft + placeholder plus error. +- In the mock site there is no 15 ft of ground. The tester backed into the office and aimed at a workbench + (5:48–5:57). The progress bar read 0% throughout, then "I can't get there" at 6:05. + +**Problems:** +- The copy says "Around your meter" for a whole-wall span. +- 14–15 ft of clear view is a big ask in many yards, and it comes from a **placeholder** rule (pool 10 ft, + `placeholder: true`). +- The request doesn't say *why* ("to check for a pool"). + +**Fix.** +- Server: don't raise a band request driven only by placeholder rules, or cap it; say which check it's for. +- App: describe the span correctly. + +### A41-1-09 (P1, server): drift makes far spots unresolvable + +The server re-run gives spot s = 10.96–13.54 ft. The unsure checks: +- AC: 3 ft 2 in ± 5 ft 7 in against a 3 ft rule. +- Window: 3 ft 0 in ± 4 ft 0 in. +- Cable run: 12 ft 7 in ± 6 ft 5 in against the 15 ft reach. + +Where the errors come from: +- `errors.drift_per_ft` = 0.16 ft per ft from the meter (`server/rules.yaml:38-40`), from ADVIO walks on a + 2018 iPhone 6s with ARKit 1.0. +- The object's error and the wall's error at the spot are **added** (`solver.py:465`), although both come from + the same walk and are strongly correlated. +- Result: 825 candidates, 0 pass, 719 unsure, 106 fail. No spot more than ~5 ft from the meter can pass a 3 ft + clearance, so every scan without a spot beside the meter ends in `manual_review`. + +**Fix direction.** +- Re-estimate drift on current phones: the field tape test, or the packet's trajectory against the tape. +- Treat errors of features measured in the same walk as relative, not summed absolutes. +- Consider measuring clearances between two features locally, from the photos that see both. + +### A41-1-10 (P2): spot on top of the existing battery + +The 11 ft phone patch (4:33–5:35) is centred on the existing Base battery, and the server's spot (10 ft 11 in +→ 13 ft 6 in) lies over the same stretch. This is inferred from frames: no tape. The app has no mark kind for +an existing battery or equipment (#27), so the server can't know it's there. The same gap was found in run 2. + +### A41-1-11 (P2): misleading "closer spot on the left" banner + +The result shows "A closer spot may exist on the left of your meter. The scan didn't reach that side". The +tester did reach the left end (the post, ~2 ft left). The end is `unexplored` because the only way out of the +corner loop was "Can't get there" (A41-1-06). The banner comes from the first `past_end` item +(`ScanEngine+Export.swift:285-293`). + +### A41-1-12 (P2): "6 more views to finish", one view shown (#39) + +At 5:43 the screen says "6 more views to finish / Take 6 more views". One request was shown (the 15 ft +ground). "I can't get there" at 6:05 went straight to the result (6:06). #39 is still present. + +### A41-1-13 (P2): "Slow down" during tilts (#26) + +"Slow down / Walk a little slower so the photos stay sharp" at 3:49, 4:08, 5:07, 5:48, 5:58 and 6:04, mostly +while tilting or turning, not walking. #26 is still present. + +### A41-1-14 (P2): "Can't get there" on a tilt task + +On "Tilt down… at your meter" the button reads "Can't get there". The tester stood at the meter and *could* +get there, so they never pressed it and stayed stuck. In code it marks ±0.5 m skipped +(`ScanEngine+Actions.swift:354-358`), which would have unblocked them. Label it "Skip this spot" or "It won't +fill" on tilt and gap tasks. + +### A41-1-15 (P2): window open/shut unanswered + +"Does this window open?" (4:22–4:28) was never answered, and "Looks complete" was accepted. `scene.json`'s +window has no `operable` field (the schema allows it). Either require an answer or state what the server +assumes. + +### A41-1-16 (P3): unprompted "leaves out 2 ft you walked" + +"Ending the wall here leaves out 2 ft you walked" sat under the strip from 1:49 to 2:00 while the task was +"Tilt down…". It is the "Wall ends here" preview (`ScanEngine+Ends.swift`), but nothing tells the user it +belongs to that button. + +### A41-1-17 (P3): "Check clearances" tick (#31) + +On the first upload the ticks all showed (5:40–5:43), because the "6 more views" screen holds. On the final +upload (6:05) "Check clearances" flashed for a frame, then the result. #31 is still present, cosmetically. + +### A41-1-18 (P3): gap patch drawn through objects + +The orange hatched patch is drawn over and through the existing battery (4:33, 5:24–5:35). This is related to +#41 and to A41-1-01. + +### A41-1-19 (P3, test): notification banners + +Notification banners covered the top card at 3:05–3:08 and 5:17–5:22. Turn on a Focus mode during recorded +runs. + +### A41-1-20 (P3): share zip size + +The share sheet reported `scan.zip` at 119.5 MB (Drive showed 114 MB). The upload to Drive was still running +when the recording ended. Consider lower JPEG quality for shared scans, or excluding the streams. + +## Timeline + +Video time; manifest time = video − 25.0 s. + +| Time | Screen / event | Narration | +|---|---|---| +| 0:00–0:14 | Control Centre, app launch, onboarding 3 pages, "Allow camera" | "run one of three … make sure that the issues persist across all three" | +| 0:17–0:22 | "Move your phone slowly", then "Find your electric meter" | No motion prompt (already granted; #43 not tested) | +| 0:23 | "Step a little closer to the wall / Then aim at your meter again" | "step a little closer to the wall. It did it!" (F-2 pass) | +| 0:25–0:29 | Close-up ring. "Which number is on your meter?" the 8-digit serial (matches the barcode) / 1S30TA10KH / 200240V3W / None, then "Meter number saved the 8-digit serial" | C-1 pass | +| 0:30–0:32 | "Take a step back" | | +| 0:33–0:54 | "Tilt down… at your meter" (g3). Ring and chevrons | "I don't know what it wants me to see … circle goes green once it's captured" | +| 0:52–0:58 | Ground cells green, g3 met at 0:54, "Walk slowly to your left", then "Tilt down… at your meter" again at 0:57 with cells amber | A41-1-04 | +| 1:14 | "What's at the left end?" (automatic) | | +| 1:17–1:44 | "It turns a corner", then the next-wall loop; accepted at 1:43.8 | "I don't understand what it wants" | +| 1:45–2:01 | "Take a step back", "Tilt down… at your meter"; "Ending the wall here leaves out 2 ft you walked" | "This is bad" | +| 2:02–2:20 | "What's at the left end?" again, "It turns a corner", refusals, **"Can't get there" 2:20**: left end −1.0 ft (unexplored) | "What is it talking about? Okay, well, can't get there" | +| 2:21–2:46 | Walked right to the far post while the card said "Tilt down… at your meter", left chevron | "That's the ground and that's the ground" | +| 2:47–2:55 | "What's at the right end?" (automatic), "It turns a corner", refusals, **"Can't get there" 2:55**: right end 20.25 ft | | +| 2:55–3:05 | "Tilt up to show above the area by your meter / 1 ft left to 5 ft right" (g12, skipped) and "What's above that part of the wall?", then "A roof edge, porch or stairs" | | +| 3:05–4:13 | "Tilt down… at your meter" (g13, g15), 65 s; the target is on the wall; tilt-up arrow at 3:44 | **3:13 "I think the ground is a little too high"**; "Claude, help" (4:01) | +| 4:13 | "Done with this wall", ground: Grass | "I don't care" | +| 4:19–4:25 | Window bottom-left and top-right; "Tap the AC unit" (#25 fixed) | | +| 4:26–4:28 | List: Window "About 7 ft right" (open/shut unanswered), AC unit "About 17 ft right". "Looks complete" | | +| 4:29–5:39 | Phone gap "Show the ground about 11 ft right…" at 0%, then 8%; patch at table height | "This experience is terrible … Where do you want me to point?" | +| 5:39.4 | "I can't get there" | | +| 5:40–5:43 | "Checking your wall" (≈3.6 s), then "6 more views to finish" | | +| 5:44–6:05 | Server gap "Show the ground out to about 15 ft from the wall / Around your meter"; aimed at a workbench in the office | "Where does it want me to be? What the flip" | +| 6:05.0 | "I can't get there", "Checking your wall", result at 6:06 (≈1 s) | | +| 6:06–6:09 | "An installer will take a look / 12 ft 3 in right of your meter, 13 ft of cable", 3D model, banners | | +| 6:10–6:25 | AR: card only, **nothing drawn** | "Where is it going? I don't know where it even is. Okay, this is awful" | +| 6:26–6:48 | Result scrolled, "Share scan", Google Drive upload | | + +## Result as displayed (matches the server re-run) + +- **Decision:** manual_review, "An installer will take a look". Summary: "More views are needed around the best + spot, 12 ft 3 in right of the meter: 7 checks depend on areas the scan did not see; a person also needs to + check distance from ac units; distance from doors and windows; cable run length." #45's split between unseen + and margin checks is now visible. +- **Pass:** + - Flush against one straight wall + - Clear of the meter's working space (9 ft 8 in ± 3 ft 3 in) + - Cable route along the wall +- **Unsure:** + - Ground under the battery (unseen) + - Gas (unseen) + - AC: 3 ft 2 in ± 5 ft 7 in + - Driveway (unseen) + - Pool (unseen) + - Doors and windows: 3 ft 0 in ± 4 ft 0 in + - No box or vent above (unseen) + - Open space in front (seen clear only to 2 ft 6 in) + - Headroom (unseen) + - Cable run length: 12 ft 7 in ± 6 ft 5 in +- **Still needed:** + - Ground from 1 ft 0 in left to 20 ft 3 in right, 14 ft 4 in out + - Wall from 6 ft to 19 ft right, 6 ft 6 in up + - Facing gap at 10 ft 11 in → 13 ft 6 in, 4 ft 10 in out + - Overhead at 10 ft 11 in → 13 ft 6 in, 6 ft 6 in up + - Keep walking past the left end (1 ft 0 in left) + - Keep walking past the right end (20 ft 3 in right) +- **Banners:** + - "A closer spot may exist on the left of your meter…" (A41-1-11) + - "Your electrical panel still needs an electrician's review…" + +## Scan summary + +| Item | Value | +|---|---| +| Duration, distance | 347.7 s of capture (0:25 → 6:05 in the video), 73.3 m walked | +| Photos | 206 kept (the counter read 206 at 6:04), 1920×1440, no depth | +| Guidance | 17 entries: 6× tilt-to-ground at the meter, 3× step back, 3× walk, 2× walk-round-corner (`cannot_reach`) plus 1 `met`, 1 tilt-up (skipped), 1 phone gap (`cannot_reach`), 1 server gap (`cannot_reach`) | +| Wall | One wall, `source: tap`, baseline −0.95 → 20.09 ft. Ends: left −1.0 ft, right 20.25 ft, both `unexplored` | +| Meter | Height exported 1.69 ft (**wrong**, real ≈ 4.8 ft); `plus_minus_ft` 0.79 (estimated plane) | +| Marks | Window span 5.71–7.96 ft, bottom 0.19 ft, top 4.06 ft (heights **wrong**, +3.15 ft for real), no `operable`. AC (folding table) 16.7–17.7 ft. No gas meter, no existing battery | +| Ground answer | Grass (lawn patches over the little ground seen: 0–1 ft and 10–11.5 ft) | +| Coverage observed | Wall: −1 → 20.2 ft (out 4.5–7.5 ft). Ground: 0–1 ft and 10–11.5 ft only. Facing: 0.7–5.7 ft out | +| Planes | 22: 18 vertical (wall, door, none), 4 horizontal: `floor` y −1.47 m, `table` y −0.52 m (**used as ground**), two small `none` | +| Coverage replay | Reach left 0 ft and right ≤ 1 ft throughout. The ground row at 0 ft out near the meter was seen by only 4 photos | + +## Test-ID results + +Matched by what was on screen; the tester didn't say IDs. Haptics unverified. + +| ID | Result | Note | +|---|---|---| +| O-1 | Pass | Onboarding, "Allow camera"; no motion prompt (#43 not exercised) | +| F-2 | **Pass** | "Step a little closer to the wall" at 0:23 | +| F-1, C-1 | Pass | Close-up and meter number the 8-digit serial | +| W-1 | **Fail (usability)** | See A41-1-03, -05, -06 | +| W-2 | Pass | The photo counter flashes green | +| W-3 | Pass | Window corners and the AC mark accepted | +| W-5 | **Fixed** | "Tap the AC unit" (#25) | +| W-7 / W-8 | **Pass (new UI)** | "What's at the right end?" / "What's at the left end?" appeared at both physical ends (#29 fixed) | +| W-9 | Known | "Slow down" during tilts (#26) | +| End placement (#24) | **Fixed** | "Can't get there" ended the wall at the walked position (−1.0 ft, 20.25 ft), not at the meter | +| G-1 | Fail | Gap progress stuck at 0% / 8% (A41-1-07, -08) | +| G-2 | Pass, copy bug | "I can't get there" uploads; "6 more views" shows one (#39) | +| U-1 | Measured | Upload 1 ≈ 3.6 s, upload 2 ≈ 1 s | +| U-2 | Known | #31 on the final upload | +| R-1 | Reached | manual_review; see above | +| R-2 | Not tried | The 3D model shown, not dragged | +| R-3 | **Fail (regression)** | Nothing drawn (A41-1-02) | +| R-4 | Not testable | Nothing drawn to occlude | +| M-1..M-3 | No tape | App values: window 5.7–8.0 ft right; AC ~17 ft right. Heights wrong by +3.15 ft (A41-1-01) | + +## Suggested order of fixes + +1. **A41-1-01 ground plane.** A small change in `groundBelow` and `LiveCapture`'s plane filter, with a big + payoff: it unblocks -03, -04, -07 and the height of -02, and corrects every height sent to the server. +2. **A41-1-02 AR view.** Restore the Canvas fallback now; debug the RealityKit path on a non-LiDAR phone. +3. **A41-1-03 / -14.** Put a timeout or auto-skip on tilt tasks, and relabel "Can't get there" on them. +4. **A41-1-06.** Add a "The wall ends here" answer and a back step; tighten the corner acceptance. +5. **A41-1-05.** Add progress feedback on the target ring and a legend for the arrows. +6. **Server A41-1-09 / -08.** Revisit drift and the placeholder-driven 15 ft request. +7. Existing: #27 (existing battery), #39, #26, #31. + +## Housekeeping + +- Worktree `C:\dev\house-scanning-beta41` (detached at `45017ba`) was created for reading code and running the + solver. Remove it with `git worktree remove ../house-scanning-beta41` when all three threads are done. +- Venvs: `%TEMP%\hs-whisper` (faster-whisper; the `medium` model is in the HF cache) and `%TEMP%\hs-tools` + (packet validator and server deps). +- Nothing was committed, pushed or posted. diff --git a/experiments/device-field-test/build-4.1/run2-2015.md b/experiments/device-field-test/build-4.1/run2-2015.md new file mode 100644 index 000000000..c07f5ad70 --- /dev/null +++ b/experiments/device-field-test/build-4.1/run2-2015.md @@ -0,0 +1,519 @@ +# Device test, build 4.1: the 20:15 recording of 2026-09-26 + +Commit-safe copy: the meter serial is removed. Frames, transcript, scan zip and server output stay local in `private/device-test-4.1-2015/` (gitignored). + +This is one of three threads analysing TestFlight 4.1. A later session compares all three and files issues. The +**issue list** is the next section and uses stable IDs (`A41-2015-…`, named after the recording time so they can't clash +with the other threads). The last column maps each finding to the earlier 20:00 analysis (`private/device-test-4.1-2000/REPORT.md`, +IDs `A41-1-…`) where one matches. + +## Inputs and how they were analysed + +| Item | Value | +|---|---| +| Video | `Downloads\20_15 9_26_2026.MP4`: **5 min 14 s** (not 7), 592×1280 at 30 fps, microphone on. Status-bar clock 20:15 → 20:20 | +| Scan | `Downloads\20_15 9_26_2026.zip`, unzipped to `scan/`: `manifest.json`, `scene.json`, 141 photos, 6 sensor streams. **It is the same run as the video**: the 35 guidance entries line up with the screen at a constant offset (**video time = manifest `t` − 727099.8 s**; capture start = video 0:10.7) | +| Build | HouseScan 0.1.0 (4.1), from `beta` `45017ba`. Code read in the detached worktree `C:\dev\house-scanning-beta41` | +| Phone | **iPhone 14 (`iPhone14,7`), no LiDAR**, iOS 26.6.2. No depth, no mesh. Battery 22% → 13% during the run | +| Site | The same indoor mock wall as the 20:00 run: meter, electrical panel, disconnect, window (~6 ft right), an **existing Base battery** (white box, ~11–13 ft right, not marked), a folding table as the AC stand-in (~13–19.5 ft right, in the corner against the wooden right wall), wooden post at the left, turf, eave | + +Working files here: `frames1s/` (1 fps), `sheets/` (16-up contact sheets, one frame per 2 s, with timestamps), `hi/` +(evidence frames; `ev_*.jpg` are the key ones), `transcript.txt` / `.json` (faster-whisper `medium`, word timestamps), +`server_rerun.json`, `siteplan.svg`. + +Checks run: +- `python -m packet validate scan` passes: packet 1.1, 141 photos at 1920×1440, 0 depth, no mesh, 13 planes, 5 marks, + 35 guidance entries, 262.8 s, 42.1 m walked. Every photo `tracking: normal`; the trajectory is `normal` from 0:11.7 to + the end, with no pose jumps (none > 0.15 m at > 2.5 m/s). +- The server solver at `beta` was re-run on `scan/scene.json` with the public demo rules. It reproduces the phone's + result exactly: `manual_review`, spot 17.10 → 19.68 ft (18 ft 5 in right), 17 ft 1 in of cable. + +## Headline + +1. **The ground was right this time.** `ground_y_m` = −1.5015 m is the ARKit `floor` plane (−1.5020 m), and the meter is + exported at 4 ft 11 in. So A41-1-01 (ground from a `table` plane) **did not reproduce**. That made the thread useful: + it separates what the table bug caused from what's broken on its own. +2. **"See it on your wall" drew nothing again.** For 20 s (4:36–4:56) the camera swept the window, the meter and + conduit (where the cable starts), the existing battery, the table and the right end. No battery, cable or tint appeared. + Tester: *"Where is that? … Where is my battery? … This is unacceptable."* A41-1-02 is **reproduced on a second run**, + with the correct ground. The 3D preview on the result screen did render. +3. **The tilt-down task at the meter still blocked the start for 70 s** (0:28–1:38), with the correct ground. The real + blocker is the coverage rule: every ground row must be seen from **two spots at least 25 cm apart**. The card only says + "Tilt down". The tester tilted in place (kept photos within ~20 cm of each other) and it finished only after they + stepped back at 1:29. Card text and arrow contradicted each other again: an up-chevron during "Tilt down" at 0:46 + and 1:20. +4. **The left wall end landed on the meter.** The tester walked left to the post, tapped "Wall ends here", then + "Something blocks it". The end went to s = 0, and the strip dropped the ~1–4 ft left of the meter it had just shown. + Cause: `WalkedEnd.end` caps the end at the farthest *kept photo* on that side, and no kept photo was taken left of + the meter. +5. **The spot is where the AC stand-in stands.** The AC was marked with one tap at the table's near-left corner, so + the scene holds a ~1 ft square at 13.1–14.1 ft. The table actually runs to ~19.5 ft. The solver then put the battery 3 ft + past that square, at 17.1–19.7 ft, which is under the table. The existing Base battery at ~11–13 ft is invisible to the + server (#27). Every spot between the window and the AC mark breaks a rule on its best estimate, so the spot went to the + far end and needs 17 ft of cable. +6. **The result text leaks an internal id**: *"objects[1] ac is 3 ft 0 in (± 6 ft 0 in) from the battery…"*. +7. **AR overlays slid off their objects.** At 4:17–4:27, after the tester had walked into the office for the server + request, the window outline was drawn over the panel and disconnect boxes. Tester: *"The window's not on the window + anymore … Now it's just a floating window. That's bad."* Tracking stayed `normal`, so this is gradual drift or a + placement bug (inferred). Either way, an AR battery drawn there would be off by feet too. + +**Better than the 20:00 run:** +- The walk to the right went all the way to the end in 86 s. Most tasks were `met`. +- The **automatic "Is this the right end of the wall?"** prompt appeared at the right end (3:15). Tester: *"Oh shoot, it + knows."* +- The window was asked "Does this window open?" and answered ("It stays shut", exported as `operable: false`). +- The close-up, the meter-number pick (the 8-digit serial, "Matches the barcode") and "What's above that part of the wall?" all worked. +- No corner-flow trap: the tester chose "Something blocks it" at both ends. + +## Issue list + +**Sev:** P0 = breaks the run or the result; P1 = major usability or correctness; P2 = moderate; P3 = polish. +**Lane:** app = iOS capture (Sam + agents); server = `server/` (Hunter + agents); test = test process. +**20:00 match:** ID in the 20:00 report; "repro" = seen again here, "not repro" = didn't happen here, "new" = only here. + +| ID | Sev | Lane | Title | Existing issue | 20:00 match | Evidence (video time) | +|---|---|---|---|---|---|---| +| A41-2015-01 | P0 | app | AR "See it on your wall" draws nothing: no battery, no cable, even with the correct ground | regression of #41 | A41-1-02 repro | 4:36–4:56 (`hi/ev_ar_empty.jpg`, `hi/ar_sheet.jpg`); 4:40–4:52 narration | +| A41-2015-02 | P1 | app+server | Spot sits on the AC stand-in: an AC is one tap with a ~1 ft footprint, so a long unit is modelled as a point at one corner | #27 (related) | new | 3:46–3:48 (`hi/ac_06.jpg`), result 4:34; `server_rerun.json` | +| A41-2015-03 | P1 | app | "Wall ends here" on the left put the end at the meter: no kept photo left of the meter, so `WalkedEnd` caps the end at 0 | #24 follow-up | new | 1:44–1:47 (`hi/ev_left_end_at_meter.jpg`); 1:45 narration | +| A41-2015-04 | P1 | app | Tilt-down at the meter held for 70 s with the correct ground: the two-spots (25 cm) rule is never explained; step-back/tilt ping-pong ×4 | #28 | A41-1-03 repro (different cause) | 0:28–1:38; 0:45, 0:56, 1:15 narration | +| A41-2015-05 | P1 | app | AR overlays drift off their objects after walking away: the window outline is drawn over the panel/disconnect | new | new | 4:17–4:27 (`hi/gap_266.jpg`, `hi/ev_server_gap.jpg`); narration 4:17–4:27 | +| A41-2015-06 | P1 | server | User-facing check text shows the internal id "objects[1] ac" | new | new | 4:56–4:58; `solver.py:1530`, `scene.py:122` | +| A41-2015-07 | P1 | server+app | Server request "Show the ground out to about 16 ft from the wall. About 19 ft right" can't be met; the span is 4–33 ft, not "about 19 ft" | new | A41-1-08 repro | 3:55–4:33 (`hi/ev_server_gap.jpg`) | +| A41-2015-08 | P1 | server | Drift makes the far spot unresolvable (AC ± 6 ft 0 in, cable ± 4 ft 3 in); a spot directly under the window comes out "unsure", not "fail" | new | A41-1-09 repro | result 4:56–5:00; `server_rerun.json` sweep | +| A41-2015-09 | P2 | app | The walk's card changes every ~3 s (22 cards in 86 s); tilt up / tilt down / walk alternate; distances jump backwards (12 → 11 → 9 ft) | new | new | 1:49–3:15; 2:21 narration | +| A41-2015-10 | P2 | server | "Still needed" and requests reach past a `limit` end: wall to 26 ft 2 in, ground to 33 ft 2 in, while the right end is a limit at 19 ft 9 in | new | new | 5:00–5:04 | +| A41-2015-11 | P2 | app | Card text and arrow disagree: up-chevron during "Tilt down" (with the correct ground) | new | A41-1-05 repro | 0:46, 1:20 | +| A41-2015-12 | P2 | app | "The blue circle" is still unexplained and shows no progress | new | A41-1-05 repro | 1:22 narration; 0:30–1:38 | +| A41-2015-13 | P2 | app | Overhead task "Tilt up to show above the area by your meter" asked from the right end, 19 ft away; the spot later needs overhead at 17–19.7 ft instead | new | related A41-1-03 copy | 3:20–3:30 | +| A41-2015-14 | P2 | app | "3 more views to finish", one request shown, then straight to the result | #39 | A41-1-12 repro | 3:54–4:34 | +| A41-2015-15 | P2 | app | "Slow down" while tilting or turning | #26 | A41-1-13 repro | 1:26, 2:44, 3:22, 4:06, 4:30 | +| A41-2015-16 | P3 | app | "Ending the wall here leaves out N ft you walked" shows under the strip during tilt tasks, unprompted | new | A41-1-16 repro | 2:02–2:06, 2:28–2:38 | +| A41-2015-17 | P3 | app | While stuck on the ground task the tester opened "Mark something" looking for a "grass" or "that's the ground" answer | new | new | 0:58–1:06 narration | +| A41-2015-18 | P3 | app | The gap patch outline 16 ft out is drawn through the office furniture | related #41 | A41-1-18 repro | 4:02–4:10, 4:30 | +| A41-2015-19 | P3 | server+app | The homeowner's summary includes "Demo rules: public values and placeholders (pool 10 ft, drive 5 ft), not Base's." | new | new | 4:34 | +| A41-2015-20 | P3 | app | The upload skips the "Check clearances" tick on the final upload | #31 (partly) | A41-1-17 repro | 4:32–4:34 | +| A41-2015-21 | P3 | test | Low Battery (20%) alert and a notification banner covered the app mid-run; battery fell 22% → 13% in ~5 min | – | A41-1-19 repro | 0:23 (`hi/lowpower.jpg`), 0:50 | +| A41-2015-22 | P3 | app | Share zip 93 MB for 141 photos | new | A41-1-20 repro | 5:04–5:12 | + +Not reproduced here (useful for the comparison): +- **A41-1-01 (ground from a table plane):** the floor was used. There was no `table` plane among the 13 planes. The two + small horizontal `none` planes sit at +0.28 m and −0.02 m, well above the meter-minus-0.3 m cut. +- **A41-1-04 (met ground task re-asked):** g9 was `met` at 1:37.9 and not re-asked. +- **A41-1-06 (corner flow trap):** not exercised; the tester chose "Something blocks it" at both ends. +- **A41-1-07 (phone gap stuck at 0%):** no phone gap was raised; "Looks complete" went straight to upload. +- **A41-1-11 ("closer spot on the left" banner):** not shown; the left end is a `limit` here. +- **A41-1-15 (window open/shut unanswered):** answered this time. + +Note for the comparison session: in this run "What's at the left end?" appeared only **after the tester tapped "Wall +ends here"** (1:45.5, transcript *"The wall ends here. Something blocks it."*). In code, `endQuestion` is set only by +`markWallEnd` and `endWallHere` (`ScanEngine+Actions.swift:123`, `ScanEngine+Ends.swift:77`). The 20:00 report calls the +left/right questions "automatic"; worth re-checking there. The **right** end's "Is this the right end of the wall?" *is* +automatic (guidance g33, `mark_end`). + +### A41-2015-01 (P0, app): AR view draws nothing + +**What happened.** +- 4:34: "An installer will take a look / 18 ft 5 in right of your meter, 17 ft of cable". The 3D model shows the AC cube, + the battery and a blue cable. +- 4:35: "See it on your wall". From 4:36 to 4:56 the camera card reads "The spot an installer will check / 18 ft 5 in + right of your meter, 17 ft of cable". +- **Nothing is drawn in any frame at 2 fps** (`hi/ar_sheet.jpg`). That includes: + - 4:46–4:52, aimed straight at the meter and its conduit, where the 17 ft cable should start; + - 4:38 and 4:54, the existing battery and the table near the spot; + - 4:40–4:44, the right end. +- The walk overlays (strip, marks) are gone too. +- Narration: *"See it on your wall. … Where is that? It's gone now. Well, the hallucination doesn't exist anymore. Where + is my battery? Claude, lock in. This is unacceptable."* (4:35–4:52). "The hallucination" is the floating window + outline of A41-2015-05, which vanished on this screen. + +**Why it matters for the diagnosis.** The 20:00 run had the ground 3 ft too high, so "the battery is drawn in mid-air or +off-screen" was a possible explanation there. Here the ground is right, and **even the cable along the wall is missing**. That +points to the RealityKit path itself (hypotheses (a)–(d) in A41-1-02: the session re-run for people occlusion drops the +meter anchor, visibility stuck off, and so on) rather than the geometry. It is now 2 of 2 non-LiDAR runs. + +**Fix direction.** As in A41-1-02: keep the SwiftUI Canvas fallback until the RealityKit entity is confirmed on screen; +don't re-run the session on this screen; log whether `showResult` returned true and whether the anchor resolved. + +### A41-2015-02 (P1, app + server): the spot is on the AC stand-in + +**What happened.** +- 3:46–3:48, "Tap the AC unit": the reticle sat on the folding table's near-left corner, beside the existing Base + battery (`hi/ac_06.jpg`). The table stands in the corner and runs from there to the wooden right wall. +- `scene.json` object: `ac`, `span_ft` 13.11–14.10, a footprint of about 0.98 ft × 0.98 ft, 0.5–1.7 ft out from the wall. +- Result: spot **17.10 → 19.68 ft**, exactly 3 ft 0 in past the AC square (the AC rule's minimum). That stretch is under + the right half of the table, the thing the tester called the AC unit. +- The earlier run tapped the same table in its middle and got an AC at 16.7–17.7 ft. So where the one tap lands decides + where the whole unit is. + +**Why the spot went to the far end** (from `server_rerun.json`, 784 candidates, 0 pass, 654 unsure, 130 fail): +- `_rank_unsure` (`solver.py:1208`) ranks candidates whose best estimate breaks a rule below all others (`estimate_fails`). +- Starts from 2.4 to ~10.3 ft are within 3 ft of the window (5.43–7.33 ft) on the estimate. Starts from ~7.5 to 17.1 ft + are within 3 ft of the AC square. +- The only start whose estimates all clear is 17.1 ft: past the AC, with 17 ft of cable. +- So the single-tap AC and the unmarkable existing battery (#27) drive the whole answer. + +**Fix direction.** +- App: mark an AC (and other boxy equipment) with **two taps** (left and right edges, as for windows), or at least a + realistic default footprint (a condenser is ~3 ft wide) centred on the tap, and say so. +- App: add "Another battery / other equipment" as a mark kind (#27). +- Server: until footprints are real, treat a single-point object as having a size error of at least half a typical unit. + +### A41-2015-03 (P1, app): the left end snapped to the meter + +**What happened** (`hi/ev_left_end_at_meter.jpg`): +- 1:37.9–1:46.4, "Walk slowly to your left". The tester walked to the post. The strip showed amber cells for ~1–4 ft + left of the meter. +- 1:45.5, **"Wall ends here"**, then "What's at the left end?", then "Something blocks it". The strip redrew with the + meter at its left edge, and every cell left of the meter was gone. The AR end pole was drawn in the turf in line with + the meter, not at the post. +- Packet: mark `wall_end_left`, `end_kind: limit`, at s = **0.0 m**. `scene.json` baseline starts 0.33 ft left of the + meter. `scene.coverage.observed` still has ground from −4.5 ft, seen from the right side. + +**Root cause.** +- `endWallHere` → `walkedEnd(.left)` → `WalkedEnd.end` (`HouseScanKit/.../Guidance/WalkedEnd.swift`): + `side.sign * (along < 0 ? reach : min(along, reach))`, where `reach` = the farthest *kept photo position* on that side. +- Kept photo positions projected on the wall (scene frame): **no kept photo has s < 0 in the whole run.** The two photos + during the left walk were at s = +1.06 ft (1:37.8) and +0.35 ft (1:43.3). Auto-capture keeps one every ~0.5 m moved, + and the phone moved less than that to the left. +- So `reach` = 0, and the end is clamped to the meter whatever the phone saw. + +**Relation to #24.** The #24 fix moved the end from "unbroken coverage" to "kept photo positions" so that ends stop +collapsing onto the meter. Short sides still collapse, because a kept photo needs 0.5 m of movement. + +**Fix direction.** +- Take the maximum of: the phone's own position, the farthest kept photo, and the farthest wall cell *seen* on that side. +- Or keep a photo when "Wall ends here" is tapped. +- Never silently drop cells the strip was showing a second earlier. + +**Impact here:** small. The post is close to the meter and the end is a `limit`. On a real wall with a few feet to the +left of the meter, a candidate side would be lost. + +### A41-2015-04 (P1, app): the ground task at the meter, with the correct ground + +The guidance log: + +| Entry | Video time | Kind | Outcome | +|---|---|---|---| +| g3 | 0:28.0–0:39.6 | tilt_to_ground ±0.30 m at the meter | superseded | +| g4 | 0:39.6–0:42.7 | step_back | met | +| g5 | 0:42.7–1:16.2 | tilt_to_ground | superseded (33.5 s) | +| g6 | 1:16.2–1:19.0 | step_back | met | +| g7 | 1:19.0–1:29.6 | tilt_to_ground | superseded | +| g8 | 1:29.6–1:32.7 | step_back | met | +| g9 | 1:32.7–1:37.9 | tilt_to_ground | **met** | + +That's **70 s** before the walk could start, down from ~151 s in the 20:00 run, with no table plane involved. + +Phone positions for the kept photos (scene frame, relative to the meter): +- 0:28–1:29: s = +0.9 to +1.5 ft, out 4.1–4.7 ft. The height dropped to 3.8–4.0 ft: the tester crouched and tilted. +- Consecutive kept views were mostly < 25 cm apart. +- At 1:29 the phone moved to 5.6 ft out, and g9 was met 8 s later. + +Why: +- `CoverageMap.add` (`HouseScanKit/.../Coverage/CoverageMap.swift:224-229`) counts a sighting of a sample row only if it + is ≥ `coveringBaseline` = **0.25 m** from the row's other sighting. A cell is covered only when **every** row of the ground + band has two such sightings. +- Tilting in place gives many views but no second position. Nothing on screen says "move a step sideways". +- The `step_back` card ("Your phone needs to see more of the wall at once") alternated with the tilt card four times. + It reads as a different request, not as "change position". + +Narration: +- *"Still tilting down. Still tilting down. I don't know what it wants from me. Claude, please."* (0:45) +- *"What do you want me to do? Claude, please."* (0:56) +- *"It's already tilted down a lot."* (1:15) +- *"I'm trying to figure out what the heck is going on."* (1:26) + +**Fix.** +- When the row has one sighting and the phone hasn't moved, say so: "Now take one step to the side and look again". + The planner knows this: one sighting, and the current position within 25 cm of it. +- Merge step-back into the same card. +- Timeout or skip as in A41-1-03. +- Consider centring the task on where the phone stands rather than ±0.30 m around the meter. + +### A41-2015-05 (P1, app): overlays slide off their objects + +At 4:17–4:27, during the server ground request, after walking ~5 m into the office and back: +- The window's dashed outline was drawn over the **panel and disconnect boxes**, ~2–3 ft left of the window + (`hi/gap_266.jpg`). +- At 4:20 it partly overlapped the window's left side (`hi/ev_server_gap.jpg`). +- Narration: *"No, it's all messed up. The window's not on the window anymore. Oh, because it was from that specific angle + and not lined up correctly. Now it's just a floating window. That's bad."* (4:17–4:27). + +What the data says: +- Tracking was `normal` throughout, with no pose jumps, so this isn't a relocalization snap. +- The window mark (5.43–7.33 ft, 3.55–6.81 ft up) agrees with the 20:00 run's window (5.71–7.96 ft) to within ~0.5 ft. + So the tap itself was roughly right. +- That leaves visual-inertial drift over 42 m of walking without LiDAR, or overlays not re-anchored to the meter anchor + after ARKit refines it (inferred, not confirmed). + +**Why it matters.** The AR result (A41-2015-01) is drawn the same way, after the same walk. Once it renders, it could be +feet off. + +**Fix direction.** +- Log the meter anchor's transform over time, and how far it moved since the tap. +- Re-derive overlay positions from the anchor every frame. +- On the result screen, ask the user to re-aim at the meter first, re-hit-test the meter plane, and correct the offset. +- Test on a non-LiDAR phone. + +### A41-2015-06 (P1, server): internal id in user-facing text + +The result screen reads (4:56–4:58): + +> Distance from AC units: objects[1] ac is 3 ft 0 in (± 6 ft 0 in) from the battery against a 3 ft 0 in rule: too close to call. + +`scene.py:122` builds the label `f"objects[{self.index}] {self.type}"`, and the solver's check `reason` embeds it. The app +shows `reason` verbatim. +- Fix: keep `subject` machine-readable, but build `reason` from a human label ("the AC unit about 14 ft right of your + meter"). +- Add a test that no `reason` / `summary` / `message` contains `objects[`. + +### A41-2015-07 (P1, server + app): the "16 ft from the wall" request + +g35, origin `server`, 3:55.5–4:33.5 (38 s): "3 more views to finish / Show the ground out to about 16 ft from the wall / +About 19 ft right of your meter. Step back and tilt down until that much ground is in view." +- The band is `ground`, span **1.27–10.10 m = 4.2–33.1 ft**, driven by the placeholder pool (10 ft) and drive (5 ft) rules + plus error. +- "About 19 ft right" is the span's middle; the span runs from 4 ft to 33 ft, 13 ft past the `limit` end. +- There is no 16 ft of ground here. The tester walked into the office, aimed at a phone booth and a workbench (4:00–4:12), + then back at the wall. The progress bar read 0% → 2% → 3%, and 20% for a moment at 4:30. +- Narration: *"About 16 feet from the wall. So where is that? … What exactly do you want me to try and see? … Well, the + place where you want me to be is in the wall, so I can't get there."* (3:56–4:31). + +This is the same issue as A41-1-08 (15 ft there). The fix is the same: don't raise a band request driven only by +placeholder rules (or cap it), say which check it's for, and describe the span correctly. + +### A41-2015-08 (P1, server): drift makes the far spot unresolvable + +Unsure checks on the chosen spot, with the size of the error: +- **AC: 3 ft 0 in ± 6 ft 0 in.** +- Doors and windows: 9 ft 9 in ± 4 ft 11 in. +- Cable run: 17 ft 1 in ± 4 ft 3 in, against 15 ft confident and 20 ft maximum. +- Meter working space passes, but at 15 ft 10 in ± 4 ft 3 in. + +**A second symptom.** The sweep marks starts 5.1–6.9 ft, a battery **directly under the window**, as `unsure` for +`opening_clearance`, not `fail`. Summed object and wall errors swamp a sub-foot distance. Ranking still avoids those +starts through `estimate_fails`, but the check outcome is wrong. + +Same root as A41-1-09: `errors.drift_per_ft` 0.16 ft/ft, and errors of same-walk features added (`solver.py:465`). + +### A41-2015-09 (P2, app): the walk's card thrashes + +From 1:49 to 3:15 (walk right, 20 ft), the card changed **22 times in 86 s**. Most cards were held exactly 3.0–3.1 s. +Sequence excerpt: + +> walk → tilt up 2 ft 3 in → walk → tilt down 4 ft 9 in → walk → tilt up 7 ft 6 in → walk → tilt down 12 ft 3 in → 11 ft +> 3 in → 9 ft 3 in → tilt up 9 ft 9 in → tilt down 9 ft 3 in → walk → tilt up 11 ft 9 in → walk → tilt down 14 ft → step +> back → tilt down 14 ft 3 in → walk → tilt down 17 ft 3 in → tilt up 19 ft 3 in → step back → "Is this the right end?" + +- The target moved **backwards** from 12 ft 3 in to 9 ft 3 in within 8 s (2:23–2:32), while the phone stood at ~12 ft. +- The tester read each card aloud and gave up following: *"I don't know exactly what it wants from me"* (2:21). +- The walk did finish, and most tasks were `met`. It works, but it feels random. + +**Fix direction.** +- Hold one instruction until it's met or clearly impossible. +- Batch "tilt up + tilt down" at the same spot into one "look up and down here" card. +- Only send the user back when the gap is behind them and they are about to finish. + +### A41-2015-10 (P2, server): requests past a `limit` end + +The right end was answered "Something blocks it" (`limit`, 19 ft 9 in). "Still needed" on the result screen (5:00–5:04): +- "Show the ground … from 4 ft 2 in right of the meter to **33 ft 2 in right**, at least 15 ft 4 in out … Part of it is + past the right end: point the camera there from the end." +- "Show the wall from 10 ft 8 in right of the meter to **26 ft 2 in right** … seen at least 6 ft 6 in up the wall. Part + of it is past the right end." + +The contract lets ground past a limit end count toward clearances, so the ground half is defensible. There is **no +wall** past a limit end, though, and asking for 6 ft of wall up to 26 ft can't be done. Clip wall-band spans to limit ends; +for ground, say why ("to check for a pool"). + +### A41-2015-11 / -12 (P2, app): arrow contradicts the card; "the blue circle" + +- An up-chevron appeared during "Tilt down to show the ground" at 0:46 and 1:20. The tester had over-tilted and was + looking at their shoes. With the correct ground, the target was simply above the view. The card should switch to + "Tilt up a little" or just "Put the ring on the circle". +- *"What is this blue circle for?"* (1:22). The ring shows no fill or tick while held on target. + +Same as A41-1-05. This run shows it isn't caused by the table ground. + +### A41-2015-13 (P2, app): overhead task asked from the far end + +- g34 (3:20.6–3:30.4): "Tilt up to show above the area by your meter / Aim at the wall from your meter to 5 ft right of + it". It was asked while the tester stood at the right end, 19 ft away. +- It was followed by "What's above that part of the wall?". The tester answered "A roof edge, porch or stairs", which + records nothing (`answerOverhead(clear: false)`), and the task was skipped. +- The server's spot then needed overhead at 17.1–19.7 ft, so the ask came at the wrong place and at the wrong time. + +Ask for overhead where the phone is, or after a candidate spot is known. Say "go back to your meter" if it must be there. + +### A41-2015-14 (P2, app): "3 more views to finish" (#39) + +At 3:54 the screen says "3 more views to finish". One request was shown. "I can't get there" at 4:31 went straight to the +result (4:34). #39 is still present. + +### A41-2015-15 (P2, app): "Slow down" (#26) + +"Slow down / Walk a little slower so the photos stay sharp" at 1:26, 2:44, 3:22, 4:06 and 4:30, mostly while tilting, +turning or crouching. #26 is still present. + +### A41-2015-16 (P3, app): unprompted "leaves out N ft you walked" + +Under the strip during tilt tasks: +- 2:02–2:06: "Ending the wall here leaves out 2 ft you walked". +- 2:28–2:38: "3 ft", then "5 ft", "4 ft", "2 ft". + +Same as A41-1-16. + +### A41-2015-17 (P3, app): the user looks for a "that's the ground" answer + +At 0:56–1:06, stuck on the ground task, the tester tapped "Mark something" (*"Mark something. There's nothing there. +There's grass option."*). The "What do you see?" sheet offers gas meter, door, window, AC unit, driveway and fence. The +ground type question only comes after "Done with this wall". + +People reach for an explicit answer when the automatic check won't finish. A "This is the ground: Grass / Concrete / …" +answer on the ground card could double as the skip. + +### A41-2015-18 (P3, app): patch outline through furniture + +The dotted outline of the 16 ft ground patch is drawn across the phone booth and the workbench (4:02, 4:10, 4:30). This is +related to #41 and A41-1-18. + +### A41-2015-19 (P3, server + app): "Demo rules" in the homeowner's summary + +The result summary ends: *"Demo rules: public values and placeholders (pool 10 ft, drive 5 ft), not Base's."* It's fine +for the team, confusing for a homeowner. Put it in a footnote or behind a flag. + +### A41-2015-20 (P3, app): "Check clearances" tick (#31) + +On the final upload (4:32–4:34) "Check clearances" still spins when the result appears. It's cosmetic. + +### A41-2015-21 (P3, test): alerts during recording + +- 0:23: the iOS "Low Battery, 20% battery remaining" alert covered the step-back card (`hi/lowpower.jpg`). +- 0:50: a calendar banner covered the card. +- The battery fell 22% → 13% over the ~5 min run, with screen recording on. + +Charge the phone and turn on a Focus mode for recorded runs. Whether the capture alone drains ~2%/min is unmeasured. + +### A41-2015-22 (P3, app): share zip size + +The share sheet showed `scan.zip` at 93 MB (Drive: 88 MB) for 141 photos. Same as A41-1-20. + +## Timeline + +Video time; manifest `t` = video + 727099.8 s. + +| Time | Screen / event | Narration | +|---|---|---| +| 0:00–0:09 | Home screen, app launch, onboarding 3 pages, "Stay safe out there", "Allow camera" | "Sam ran out of usage … All of the issues from the previous one have been resolved, right? Yep." | +| 0:10–0:15 | "Find your electric meter", "This is my meter" | | +| 0:16–0:20 | Close-up ring (g1). "Which number is on your meter?" the 8-digit serial "Matches the barcode", then "Meter number saved" | "That's my barcode" | +| 0:20–0:28 | "Take a step back" (g2); Low Battery alert at 0:23 | "Low power mode" | +| 0:28–1:38 | "Tilt down… at your meter" ×4 with "Take a step back" ×3 in between; up-chevron 0:46, 1:20; banner 0:50 | "I don't know what it wants from me. Claude, please" (0:45); "It's already tilted down a lot" (1:15); "What is this blue circle for?" (1:22) | +| 0:58–1:06 | "What do you see?" sheet opened, then closed | "Mark something. There's nothing there. There's grass option" | +| 1:37.9 | g9 `met`; "Walk slowly to your left" | | +| 1:45.5 | **"Wall ends here"**, "What's at the left end?", "Something blocks it": left end at s = 0 (the meter) | "The wall ends here. Something blocks it." | +| 1:49–3:15 | Walk right: 22 cards (walk / tilt up / tilt down / step back); "leaves out N ft" under the strip | "I don't know exactly what it wants from me" (2:21) | +| 3:15 | **Automatic "Is this the right end of the wall?"** (g33), "Wall ends here", "Something blocks it": right end 19.78 ft (limit) | "Oh shoot, it knows" | +| 3:20–3:30 | "Tilt up to show above the area by your meter" (g34) from the right end; "What's above…?", "A roof edge, porch or stairs" | | +| 3:30–3:33 | "That's the whole wall", "Done with this wall"; ground: Grass | | +| 3:36–3:44 | Window bottom-left and top-right; "It stays shut" | | +| 3:46–3:48 | "Tap the AC unit", reticle on the folding table's near-left corner; "AC unit, About 14 ft right" | "That's the AC unit" | +| 3:50–3:54 | "Looks complete", "Getting your measurements ready", "Checking your wall" (~4 s) | | +| 3:54–4:31 | "3 more views to finish / Show the ground out to about 16 ft… About 19 ft right" (g35), 0–3% (20% at 4:30); into the office and back; window outline drifts off the window | "So where is that? … The window's not on the window anymore … floating window. That's bad." | +| 4:31–4:34 | "I can't get there", "Checking your wall" (~2 s), result | "the place where you want me to be is in the wall, so I can't get there" | +| 4:34 | "An installer will take a look / 18 ft 5 in right of your meter, 17 ft of cable"; 3D model | | +| 4:35–4:56 | "See it on your wall": card only, **nothing drawn** | "Where is my battery? … This is unacceptable." | +| 4:56–5:04 | "What we checked" and "Still needed" scrolled | "Lots of things, lots of things" | +| 5:04–5:14 | "Share scan", 93 MB zip, Google Drive upload | | + +## Result as displayed (matches the server re-run) + +- **Decision:** manual_review, "An installer will take a look". Summary: "More views are needed around the best spot, 18 ft + 5 in right of the meter: 8 checks depend on areas the scan did not see; a person also needs to check distance from ac + units; cable run length. Demo rules: public values and placeholders (pool 10 ft, drive 5 ft), not Base's." +- **Spot:** 17.10 → 19.68 ft, 2 ft 7 in wide; cable route 17 ft 1 in along the wall. +- **Pass:** + - Flush against one straight wall + - Clear of the meter's working space (15 ft 10 in ± 4 ft 3 in) + - Cable route along the wall +- **Unsure (unseen):** + - Ground under the battery + - Gas (3 ft) + - Driveway (5 ft) + - Pool (10 ft) + - Doors and windows (9 ft 9 in ± 4 ft 11 in, but the area wasn't fully seen) + - No box or vent above + - Open space in front + - Headroom +- **Unsure (margin):** + - AC: "objects[1] ac is 3 ft 0 in (± 6 ft 0 in)" + - Cable run 17 ft 1 in ± 4 ft 3 in +- **Still needed:** + - Ground from 4 ft 2 in to 33 ft 2 in right, 15 ft 4 in out + - Wall from 10 ft 8 in to 26 ft 2 in right, 6 ft 6 in up + - Facing gap from 19 ft 0 in to 19 ft 8 in, 4 ft 10 in out + - Overhead from 17 ft 1 in to 19 ft 8 in, 6 ft 6 in up +- **Banner:** "Your electrical panel still needs an electrician's review…" +- **Solver stats:** 784 candidates, 0 pass, 654 unsure, 130 fail. + +## Scan summary + +| Item | Value | +|---|---| +| Duration, distance | 262.8 s of capture (0:10.7 → 4:33.5 in the video), 42.1 m walked | +| Photos | 141 kept, 1920×1440, no depth, all `tracking: normal` | +| Guidance | 35 entries: 12× tilt_to_ground (4 at the meter), 6× step_back, 8× walk, 7× gap_band (6 phone, 1 server), 1 close-up, 1 mark_end | +| Wall | One wall, `source: tap`, baseline −0.33 → 19.78 ft. Ends: left 0.0 m (the meter; `limit`), right 6.03 m = 19.78 ft (`limit`) | +| Meter | 4.93 ft above the ground (correct: floor plane); `plus_minus_ft` 0.79 | +| Ground | `ground_y_m` −1.5015 m = the `floor` plane (−1.5020 m, 10.9 m × 5.7 m) | +| Marks | Window 5.43–7.33 ft, 3.55–6.81 ft up, `operable: false`. AC (folding table) 13.11–14.10 ft, one tap, ~1 ft square. No gas, no existing battery | +| Ground answer | Grass | +| Coverage observed | Wall 0 → 19.8 ft (out 5–7.5 ft). Ground −4.5 → 19.8 ft, 0.5–7 ft out near the meter, 4.5–5 ft out over 7.5–14 ft, 15.5–16.5 ft out past 14 ft | +| Planes | 13: 10 vertical (wall, none), 3 horizontal (`floor` −1.50 m, two small `none` at +0.28 and −0.02 m) | + +## Test-ID results + +Matched by what was on screen; the tester didn't say IDs. Haptics unverified. + +| ID | Result | Note | +|---|---|---| +| O-1 | Pass | Onboarding, "Allow camera" | +| F-1, C-1 | Pass | Close-up and meter number the 8-digit serial ("Matches the barcode") | +| F-2 | Not seen | No "Step a little closer" refusal this run | +| W-1 | **Fail (usability)** | A41-2015-04, -09, -11, -12 | +| W-2 | Pass | The photo counter flashes green | +| W-3 | Pass | Window corners and the AC mark accepted; the window answered | +| W-5 | Pass | "Tap the AC unit" | +| W-7 / W-8 | Pass (right), **wrong position (left)** | Right end: automatic prompt. Left end: at the meter (A41-2015-03) | +| W-9 | Known | "Slow down" during tilts (#26) | +| End placement (#24) | **Partly** | Right end at the walked position; left end collapsed to the meter | +| G-1 | Fail | Server gap stuck at 0–3% (A41-2015-07) | +| G-2 | Pass, copy bug | "I can't get there" uploads; "3 more views" shows one (#39) | +| U-1 | Measured | Upload 1 ≈ 4 s, upload 2 ≈ 2 s | +| U-2 | Known | #31 on the final upload | +| R-1 | Reached | manual_review, spot on the AC stand-in (A41-2015-02) | +| R-2 | Not tried | 3D model shown, not dragged | +| R-3 | **Fail (regression)** | Nothing drawn (A41-2015-01) | +| R-4 | Not testable | Nothing drawn to occlude | +| M-1..M-3 | No tape | Window 5.4–7.3 ft right (20:00 run: 5.7–8.0 ft); meter 4 ft 11 in up | + +## Suggested order of fixes (this run's view) + +1. **A41-2015-01 AR view.** Now 2 of 2 non-LiDAR runs, with the correct ground. Restore the Canvas fallback. +2. **A41-2015-02 AC footprint + #27.** One tap on long equipment puts the battery on top of it. This is the most direct + cause of a wrong spot. +3. **A41-2015-04 / -11 / -12.** Explain "two places" (tell the user to step sideways), and make the arrow and card agree. + This is what blocks the start even when the ground is right. +4. **A41-2015-06.** A small server fix: human labels in `reason`. +5. **A41-2015-05.** Instrument anchor drift; required before the AR result can be trusted. +6. **A41-2015-03.** Include the phone position and seen cells in `WalkedEnd`. +7. **A41-2015-09.** Hold instructions longer; merge tilt up/down. +8. **Server A41-2015-07 / -08 / -10.** Placeholder-driven requests, drift model, clip to limit ends. +9. Existing: #39, #26, #31. + +## Housekeeping + +- Code was read in the existing worktree `C:\dev\house-scanning-beta41` (detached at `45017ba`). The solver re-run and + the packet validator used `%TEMP%\hs-tools`; transcription used `%TEMP%\hs-whisper`. +- Nothing was committed, pushed or posted. diff --git a/experiments/device-field-test/build-4.1/run3-2055.md b/experiments/device-field-test/build-4.1/run3-2055.md new file mode 100644 index 000000000..afeb57857 --- /dev/null +++ b/experiments/device-field-test/build-4.1/run3-2055.md @@ -0,0 +1,444 @@ +# Device test, build 4.1: the 20:55 recording of 2026-09-26 (outdoors, at night) + +Commit-safe copy: the meter numbers are removed. The photos and video show the street number, street name and meter number, so they stay local in `private/device-test-4.1-2055/` (gitignored) and must never be attached to a public issue. + +This is one of three threads analysing TestFlight 4.1. A later session compares the three and files issues. The **issue +list** below uses stable IDs (`A41-2055-…`, named after the recording so they can't clash with other threads). The last +column maps each finding to the earlier reports where one matches: 20:00 (`private/device-test-4.1-2000/REPORT.md`, +`A41-1-…`) and 20:15 (`private/device-test-4.1-2015/REPORT.md`, `A41-2015-…`). + +**This run is short and unfinished.** The tester was stuck on the first task for 73 s and said *"I'm just gonna end +the test here"*. They then tapped "Can't get there" through every remaining task, got a result and shared the zip. The +recording stops during the Google Drive upload, at 2:43, with the phone at 2% battery. The wall walk never happened, +and "See it on your wall" (the AR view) was never opened. Even so, the run exposes the most serious bug yet (A41-2055-01). + +## Inputs and how they were analysed + +| Item | Value | +|---|---| +| Video | `Downloads\20_55 9_26_2026.MP4`: **2 min 43 s**, 1170×2532 at 60 fps, microphone on. Battery **12% → 2%** | +| Scan | `Downloads\20_55 9_26_2026.zip`, unzipped to `scan/`: `manifest.json`, `scene.json`, 65 photos, 6 sensor streams. **It is the same run as the video**: the 10 guidance entries line up with the screen at **video time ≈ capture time + 6.6 s** (capture start = video 0:06.6; capture `started_at` 01:52:51Z = 20:52:51 CDT) | +| Build | HouseScan 0.1.0 (4.1), from `beta` `45017ba`. Code read in the detached worktree `C:\dev\house-scanning-beta41` | +| Phone | **iPhone 14 (`iPhone14,7`), no LiDAR**, iOS 26.6.2. No depth, no mesh. Same phone as the 20:00 and 20:15 runs | +| Site | **A real house, outdoors, after dark**, lit by a porch light. Gravel ground, a meter on a wooden wall with conduit, a grey disconnect box, an AC condenser nearby (seen 0:54–1:02), a white planter/box, a yard beyond. The tester's words: *"third test run"* | + +Working files here: `frames1s/` (1 fps), `sheets/` (16-up contact sheets, one frame per 2 s, timestamped), `hi/` +(evidence; `ev_*.jpg` and `anchor_*.jpg` are the key ones; `burst_sheet1-6.jpg` are 4 fps from 1:26 to 1:48), +`transcript.txt` / `.json` (faster-whisper `medium`, word timestamps), `server_rerun.json`, `siteplan.svg`. + +Checks run: +- `python -m packet validate scan` passes: 65 photos, 0 depth, no mesh, 8 planes, 3 marks, 10 guidance entries, + 98.4 s, 27.0 m walked. Every photo `tracking: normal`. +- The server solver at `beta` was re-run on `scan/scene.json` with the public demo rules. It **reproduces the phone's + result exactly**: `manual_review`, spot −5.90 → −3.32 ft (4 ft 7 in left of the meter), 3 ft 4 in of cable, 7 unsure + checks. 453 candidates: 0 pass, 393 unsure, 60 fail. +- The meter anchor was projected into the close-up photos and compared with ARKit's own detected planes (see A41-2055-01). + +## Headline + +1. **The meter tap put the whole wall in the wrong place** (P0). "This is my meter" hit an ARKit *estimated* plane + (`walls[0].source: tap`) in low light. The anchor is **~1.1 m behind the real wall** (inside the house) and the wall + line is **turned ~46°** from the real wall. ARKit's own detected wall planes, in the same packet, disagree with it by + that much. Nothing checks for this. Everything downstream is built on it: the ground task, the strip, the ends and + the result. +2. **The result puts the battery inside the house** (P0/P1). The spot "4 ft 7 in left of your meter" has its centre + **~1.9 m behind the detected house wall**. Yet it passes "Flush against one straight wall", "Clear of the meter's + working space" and "Cable route along the wall". The wall was never walked, both ends are `unexplored`, and both + walk tasks were refused. The server still produced a spot and a 3D model and offered "See it on your wall". +3. **The first ground task was impossible and ran for 73 s** (0:21–1:34, g3). "Tilt down to show the ground. The strip + along the wall, at your meter." It was never met. The likely cause (inferred) is point 1: the strip it wants sits + 1.1 m inside the wall. From where the tester stood (~2 m from the real wall, ~3 m from the app's wall), the foot of + the band is at the 65° grazing limit. This is the third run in a row where the first tilt-down task blocks the start. +4. **"It's too dark to see the wall" flickered at least 9 times** in 80 s, replacing the task card and usually hiding + its "Can't get there" button. It asks for a porch light that was already on. Kept photos have mean luma 44–99 + (median 57) against a gate of 40: the scene sits right on the threshold. Tester: *"I cannot turn on more light. I'm + sorry."* +5. **There's no way to end a scan early.** To stop, the tester tapped "Can't get there" **7 times in ~2.5 s** (1:34–1:36): + tilt down, walk left, walk right, tilt up, and three more tilt-down cards. Each tap silently set an end or skipped a + task, because each new card's button appears under the same finger. The result is a 14.9 ft "wall" + (13 ft 2 in left → 1 ft 9 in right) that nobody walked. + +**What worked:** +- Onboarding and camera permission. +- The meter number pick: the correct number (the correct 7-digit number) was offered first, next to one wrong candidate (a wrong 6-digit number). +- Upload and check were quick: "Checking your wall" took ~2 s each time. +- The result screen and the 3D preview rendered. +- The correct ground was chosen. There are two `floor` planes (−1.553 m and −1.806 m); the app used −1.553 m, and the + phone heights (1.3–1.6 m above it) confirm that. **A41-1-01 was not reproduced.** + +## Issue list + +**Sev:** P0 = breaks the run or the result; P1 = major usability or correctness; P2 = moderate; P3 = polish. +**Lane:** app = iOS capture (Sam + agents); server = `server/` (Hunter + agents); test = test process. +**Earlier match:** ID in the 20:00 (`A41-1-`) or 20:15 (`A41-2015-`) report; "repro" = seen again here, "new" = only here. + +| ID | Sev | Lane | Title | Existing issue | Earlier match | Evidence (video time) | +|---|---|---|---|---|---|---| +| A41-2055-01 | P0 | app | Meter tap on an ARKit *estimated* plane put the wall line ~1.1 m behind the real wall and ~46° off; no sanity check, never corrected when real wall planes were detected | new | new | 0:10–0:14; `hi/anchor_sheet.jpg`; manifest planes vs `meter_anchor` | +| A41-2055-02 | P0 | server+app | The result places the battery ~1.9 m inside the house and passes wall-backing, meter-space and cable-route checks on a wall that was never walked (source `tap`, both ends unexplored, walk refused both ways) | new | new | 1:46–1:48 (`hi/ev_result.jpg`); `server_rerun.json` | +| A41-2055-03 | P1 | app | The first "Tilt down… at your meter" task ran 73 s, never met, no timeout; the tester gave up on the run | #28 | A41-1-03, A41-2015-04 repro (3rd of 3 runs; likely a different cause here: -01) | 0:21–1:34; 0:48, 1:30 narration | +| A41-2055-04 | P1 | app | "It's too dark to see the wall" flickers (≥ 9 times), covers the task card, hides "Can't get there", says "wall" while aiming at the ground, and asks for a light that's already on | new | new | 0:26, 0:36, 0:44, 0:48–0:50, 1:26–1:28, 1:30.5, 1:32.75, 1:38.25, 1:43.75 (`hi/ev_dark_card_no_button.jpg`); 0:27, 0:48 narration | +| A41-2055-05 | P1 | app | No "finish now" option: ending early took 7 rapid "Can't get there" taps; each new card's button lands under the finger, so taps chain with no pause or confirmation, and the refused walks set unexplored ends | new | new | 1:34–1:36.5 (`hi/ev_skip_cascade.jpg`); 1:30–1:39 narration | +| A41-2055-06 | P2 | app | Ends from "Can't get there" don't match where the phone went: left end 13 ft 2 in left (phone never past ~6.7 ft left in the exported frame), right end 1 ft 9 in (phone at ~5 ft right, farthest kept view ~6 ft) | #24 follow-up | related A41-2015-03 | 1:34.25–1:35.0; marks `wall_end_left/right` | +| A41-2055-07 | P2 | app | "Step a little closer to the wall" shown with the phone ~0.4 m from the meter (it means "no vertical plane found") | new | new | 0:12 (`hi/ev_step_closer_at_meter.jpg`); 0:11.7 narration | +| A41-2055-08 | P2 | app | The overhead card's "Can't get there" button overlaps its 3-line detail text | new | new | 1:35.25 (`hi/ev_overhead_button_overlap.jpg`) | +| A41-2055-09 | P2 | server | "A closer spot may exist on the left of your meter. The scan didn't reach that side." while the spot is itself on the left | new | A41-1-11 repro | 1:46–1:47 | +| A41-2055-10 | P2 | server+app | Server request "Show the ground out to about 14 ft from the wall. Around your meter." on a scan with no walked wall; "4 more views to finish", one shown | #39 | A41-1-08, A41-2015-07, A41-2015-14 repro | 1:42–1:45 (`hi/ev_server_gap_dark.jpg`) | +| A41-2055-11 | P2 | app | An AC condenser near the meter was in view for ~8 s and never offered for marking; "Anything else near your meter?" said "Nothing marked yet" and the result says an AC "could hide there" | #27 (related) | new | 0:54–1:02; 1:38.5; 1:47 | +| A41-2055-12 | P3 | server | Grammar: "so a AC unit could hide there" | new | new | 1:47.25; `server_rerun.json` `ac_clearance` | +| A41-2055-13 | P3 | app | "Looks complete" sends with the ground type unanswered; ground under the battery then goes to review | new | new | 1:38.5–1:39.5 | +| A41-2055-14 | P3 | app | "Ending the wall here leaves out 2 / 5 / 8 / 11 ft you walked" under the strip while the tester walked *away* from the wall into the yard, not along it | new | A41-1-16, A41-2015-16 repro | 0:38, 0:54–0:56, 1:10–1:14 | +| A41-2055-15 | P3 | app | "Slow down" while tilting | #26 | A41-1-13, A41-2015-15 repro | 0:54 | +| A41-2055-16 | P3 | server+app | The summary includes "Demo rules: public values and placeholders (pool 10 ft, drive 5 ft), not Base's." | new | A41-2015-19 repro | 1:46 | +| A41-2055-17 | P3 | test | Battery 12% → 2% in 2:43 with screen recording; the recording ended during the upload, probably because of the battery (inferred) | – | A41-1-19, A41-2015-21 repro | 0:00 (12%), 2:42 (2%) | +| A41-2055-18 | P3 | test | Privacy: the kept photos and the meter close-up show the street number, street name and meter number. Fine in `private/`; never attach them to a public issue | – | new | 0:08–0:20; `scan/photos/p00001–p00005` | + +Not reproduced or not tested here (useful for the comparison): +- **A41-1-01 (ground from a `table` plane):** not reproduced. The higher of two `floor` planes was used, and it is the + right one. +- **A41-1-02 / A41-2015-01 (AR view draws nothing):** **not tested**. "See it on your wall" was never tapped. +- **A41-2015-05 (overlays drift):** not observable; the walk overlays were only on screen near the meter. +- **A41-2015-02 (spot on the one-tap AC):** not applicable; nothing was marked. +- **A41-2015-09 (walk card thrashing):** not applicable; no walk. +- **Step-back / tilt ping-pong (A41-2015-04):** **not seen**. After the first "Take a step back" (g2, met), the single + tilt task g3 stayed up for 73 s without alternating. + +### A41-2055-01 (P0, app): the meter tap put the wall in the wrong place + +**What happened.** +- 0:10: "This is my meter". The card changed to "Step a little closer to the wall / Then aim at your meter again". + That is `aimAtWallForMeter`: the raycast found no vertical plane. The phone was already ~0.4 m from the meter. +- 0:13: the second tap was accepted. The hit came from `.estimatedPlane`, since the scene's `walls[0].source` is `tap` + (`ScanEngine+Export.swift` `lineSource`). ISO was 3200 under a porch light. + +**Where the anchor is** (`session.meter_anchor.pose_in_world` against the photos and planes in the same packet): +- In the close-up photos p00001–p00005, the anchor projects onto the meter's line of sight (`hi/anchor_sheet.jpg`, red + circle) but **1.6 m from the camera**. The meter's glass (~0.18 m) spans ~880 px at fx 1475, which puts the real + meter **~0.35 m** from the camera. So the anchor is ~1.2 m too deep. +- ARKit detected the house wall during the scan: a 6.1 m × 2.2 m `wall` plane, a 1.25 m × 1.65 m plane at the meter + board, and two small ones. All four have normals within ~10° of each other, about (0.55, 0, 0.84). +- Signed distance to those planes: + - Phone at the close-up: +0.36 to +0.42 m (in front, as it should be). + - Meter anchor: **−1.09 to −1.15 m (behind the wall)**. +- The anchor's wall normal is (−0.25, 0, 0.97), **~46–48°** from the detected wall planes. The camera faced the meter + square-on (its forward is anti-parallel to the detected planes' normals), so the tap plane's orientation is what's + wrong, not the view. + +**Why nothing caught it.** +- `markMeter` (`ScanEngine+Actions.swift:14-60`) accepts the first `.estimatedPlane` hit with no checks: not how far + the hit is compared with where the phone is, not how the normal compares with the camera's facing direction, and not + how it compares with detected vertical planes nearby. +- The wall frame is never re-fitted later, even though ARKit then detected a 6.1 m `wall` plane that disagrees by + ~1.1 m and ~46°. +- On a non-LiDAR phone there is no depth, so `CoverageMap.sampledRows` can't notice that the "wall" it claims to see + is really the house wall seen through an imaginary plane. + +**What it broke downstream.** +- The ground task at the meter (A41-2055-03). +- The strip went almost all green right after the skips (1:35): 13 ft of "seen wall" on a line that cuts through the + yard at 46°. +- Both ends (A41-2055-06). +- The result (A41-2055-02). + +**Fix direction.** +- Prefer `.existingPlaneGeometry`. Take an estimated plane only after sanity checks: + - the hit distance agrees with the close-up's apparent meter size (or is within ~1.5 m of the phone); + - the normal is within ~25° of the camera's reversed forward direction; + - the hit is within a few cm of any detected vertical plane that covers it. + Otherwise say "Move the phone slowly side to side so it can find the wall" and wait. +- When a detected `wall` plane later covers the meter's pixel and disagrees by more than ~0.2 m or ~10°, re-fit the + wall line to it, or at least flag the scan. +- Export the tap source's disagreement with the detected planes, so the server can widen the wall error (see -02). + +### A41-2055-02 (P0/P1, server + app): the battery lands inside the house + +The result (1:46, `hi/ev_result.jpg`, matched by `server_rerun.json`): +- **"An installer will take a look / 4 ft 7 in left of your meter, 3 ft of cable"**. +- The 3D model shows the battery beside the meter, and the "See it on your wall" button is offered. +- "What we checked": + - **Pass:** + - "Flush against one straight wall: The whole footprint backs onto one straight, observed wall segment." + - "Clear of the meter's working space" (2 ft 1 in, ± about 2 ft). + - "No box or vent above". + - "Open space in front". + - "Cable route along the wall". + - "Cable run length" (3 ft 4 in ± 2 ft 0 in). + - **Unsure:** ground, gas, AC, driveway, pool, doors and windows, headroom. + +In world terms, the spot's centre is **1.8–1.9 m behind** the detected house-wall planes. That's inside the house. +Every "pass" above follows from the imaginary wall line. + +The server had enough to know this scan can't support a spot: +- `walls[0].source: tap`. +- `ends.left.kind` and `ends.right.kind` are both `unexplored`. +- No `ground` entries, no objects. +- 98 s of capture, one wall "observed" only through the tap plane. + +The planes that contradict the wall line are in the manifest, but the server never sees the manifest. + +**Fix direction.** +- Server: when the wall line is `tap`-sourced and not confirmed by a walk (no kept views along it, or both ends + `unexplored` with ~0 walked), don't return a spot. Return "not enough scan: walk the wall" (or `reject`/`incomplete` + with a reason). At minimum, wall backing and route should be `unsure`, not `pass`. +- App: don't offer "See it on your wall" or a 3D spot when the walk was refused in both directions. Show "We couldn't + measure the wall. Try again in daylight" instead. +- App: send the plane-disagreement measure from -01 so the server can act on it. + +### A41-2055-03 (P1, app): the first tilt-down task was impossible + +Guidance log (capture time +6.6 s = video): + +| Entry | Video time | Kind | Outcome | +|---|---|---|---| +| g1 | 0:12.7–0:16.5 | closeup | met | +| g2 | 0:16.6–0:21.2 | step_back | met | +| g3 | **0:21.2–1:34.3** | tilt_to_ground ±0.30 m at the meter | **cannot_reach (73.1 s)** | +| g4–g9 | 1:34.3–1:36.5 | walk left, walk right, overhead, tilt ×3 | all cannot_reach, 0.2–0.5 s each | +| g10 | 1:43.8–1:45.0 | server gap_band ground "about 14 ft" | cannot_reach | + +During g3: +- The tester swept the gravel at the foot of the wall many times, from 1.5–2 m away. The ring sat on the gravel for + seconds at a time (0:24, 0:32–0:34, 0:40, 0:48–0:50, 1:18–1:32). +- They also walked ~20 ft out into the yard and back (0:57–1:16), still on the same card. +- Narration: *"Tilt down to show the ground. I cannot turn on more light. I'm sorry."* (0:48). *"So that's unfortunate. + I'm just gonna look around."* (0:53). *"You want me to do that? … Okay. Well, can't get there. I'm just gonna end the + test here."* (1:27–1:30). + +Why (inferred from the geometry, not a device log): +- A ground row counts only if it is seen within `maxAngleFromNormal` = 65° of straight up (`CoverageMap.sampledRows`) + from two spots ≥ 0.25 m apart. For a phone held ~1.5 m up, that means within ~3.2 m horizontally. +- In the app's frame the phone stood ~9–10 ft (2.8–3.1 m) out and ~5 ft to the side, so the band's wall-foot row was + ~3.2–3.4 m away: at or past the limit. The foot row lies 1.1 m inside the real wall (-01). +- In the real world the tester stood a normal ~2 m from the wall. With a correct anchor the foot row would have been + at ~37° elevation and counted. +- The dark gate (A41-2055-04) also dropped frames: 65 kept photos in 98 s, with gaps of 5–9 s at 17.5–23 s, 26–35 s and + 40–46 s of capture. + +This is the third run where the first tilt-down task blocks the start: +- 20:00: ground taken from a table plane. +- 20:15: the 25 cm two-spots rule, unexplained. +- Here: the wall line itself. + +Whatever the cause, the task needs a timeout and a real way forward, e.g. after ~20 s offer "Skip this, start walking". + +### A41-2055-04 (P1, app): the "too dark" card + +- The coaching comes from `AutoCapture.evaluate`: `meanLuma < minMeanLuma` (40) → `.skip(.tooDark)` → + `walkCoaching` → "It's too dark to see the wall / Turn on a porch light, or try again in daylight." +- The debounce is 0.7 s on and 0.5 s off, so a scene near the threshold flickers. Kept photos have luma 44–99 + (median 57), just above the gate. Frames aimed at shadowed gravel fall under it. + +Problems: +- **Flicker**: at least 9 appearances in 80 s; the task card and the dark card alternate every few seconds. +- **It hides the escape**: at 1:26.25–1:28.25 the dark card has no "Can't get there" button (`hi/ev_dark_card_no_button.jpg`). + At 1:30.5 it is half faded in. At 1:44.0 (server request) it's there. So it's inconsistent at best. +- **Wrong words**: it says "see the wall" while the task is the ground. +- **An impossible action**: the porch light was on. Tester: *"I cannot. That's the light there."* (0:27), *"I cannot + turn on more light. I'm sorry."* (0:48). +- The ring and arrow stay on screen under the dark card, so the user can't tell whether the task is still active. + +**Fix direction.** +- Use hysteresis (e.g. show under 40 for 2 s, clear above 48) and keep the task card with a small "low light" badge + instead of replacing it. +- Offer the flashlight (a torch toggle; check whether it works alongside the AR session on non-LiDAR phones). +- Never hide "Can't get there". +- If more than half the frames are dark for 20 s, say it once, plainly: "It's too dark to measure. Try in daylight" + with a way out. + +### A41-2055-05 (P1, app): ending the run early + +- At 1:30 the tester decided to stop: *"I'm just gonna end the test here."* No control does that. +- From 1:34.0 to 1:36.5 they tapped "Can't get there" at the same screen spot, **7 times** (*"Can't get there. Can't + get there. Can't get there. Can't get there. Can't get there. Done."*). +- Each tap resolved one card, and the next card's button appeared under the finger within 0.2–0.5 s: + - g3: tilt down at the meter. + - g4: walk left, so the left end was set, `unexplored`, at 13 ft 2 in left. + - g5: walk right, so the right end was set, `unexplored`, at 1 ft 9 in right. + - g6: overhead. + - g7–g9: tilt-down at 8 ft 9 in, 12 ft and 5 ft 6 in left. +- Then came "That's the whole wall", "Done with this wall", "Looks complete", the server request, "I can't get there" + and the result. + +Problems: +- Refusing two walks in a row is a strong signal. The app should ask "End the scan here?", not silently write two ends. +- A button that stays under the finger across successive cards invites accidental skips even when the user doesn't + mean it. Add a ~0.6 s input lock on a new card, or move or animate the button. +- Add an explicit "Finish scan" (behind a confirm) in a menu, so testers don't have to abuse "Can't get there". + +### A41-2055-06 (P2, app): the ends don't match the phone + +- The left end (`wall_end_left`, s = **−4.005 m = −13.14 ft**) was set by "Can't get there" on "Walk slowly to your + left" while the phone was on the right side of the meter. `WalkedEnd.end` should then use the farthest kept view on + the left. Projected in the exported frame, that is **−6.7 ft** (p00039, the yard detour); the whole trajectory's + minimum is also −6.7 ft. +- The right end (s = **+0.52 m = +1.72 ft**) was set by "Can't get there" on "Walk slowly to your right". The phone was + at ~+5 ft and the farthest kept right view at +6.0 ft, so `min(along, reach)` should give ~+5 ft. +- These don't reconcile. Possible explanations: + - ARKit moved the meter anchor during the run (the export uses the final pose), since the anchor was on an estimated + plane. + - `observedCameras` holds positions other than the kept photos. +- The device log line `can't get there on the left: end at s=…, phone at s=…, walked … m, covered reach … m` + (`ScanEngine+Ends.swift:95`) would settle it. **Needs the device log**; not confirmed as a bug. +- Either way, a wall end 13 ft from anywhere the phone went is wrong on its face. + +### A41-2055-07 (P2, app): "Step a little closer to the wall" + +At 0:10 the tester was ~0.4 m from the meter, filling the frame, and was told to step closer. The copy is `aimAtWallForMeter`, used +for "no vertical plane under the tap". The tester stepped in and tapped again, which is what produced the bad +estimated-plane hit (-01). Better copy: "Hold on, finding your wall… move the phone slowly side to side". Better +behaviour: wait for a detected plane rather than accept the next estimated one. + +### A41-2055-08 (P2, app): button overlaps the overhead card text + +At 1:35.25 "Tilt up to show above the area by your meter / Aim at the wall from 5 ft left to 2 ft right of your meter +and whatever is above it": the "Can't get there" pill sits on top of "above it" (`hi/ev_overhead_button_overlap.jpg`). +Other 3-line cards (1:35.5, "8 ft 9 in left of your meter") push the button down correctly, so it's this card's layout. + +### A41-2055-09 (P2, server): "a closer spot may exist on the left" + +The banner under the result reads "A closer spot may exist on the left of your meter. The scan didn't reach that +side.", but the spot is already 4 ft 7 in left, and the scan "reached" 13 ft left. It comes from the unexplored left +end (`reasons` `unexplored_end`). The copy should name the end ("past 13 ft left") or be dropped when the spot is on +that side. It was seen at 20:00 too (A41-1-11). + +### A41-2055-10 (P2, server + app): the server's "14 ft" request + +- 1:42: "4 more views to finish". +- 1:44: "Show the ground out to about 14 ft from the wall / Around your meter. Step back and tilt down until that + much ground is in view." +- It's driven by the placeholder pool and driveway rules (band out 13 ft 1 in). +- The first card was the dark card again (1:43.75). +- The tester tapped "I can't get there" within a second (*"Let me save this. Can't get there."*). + +Same as A41-1-08, A41-2015-07 (placeholder-driven band) and #39 ("4 more views", one shown). + +### A41-2055-11 / -12 (P2 / P3): the AC condenser + +- A condenser sits by the disconnect box (0:54–1:02, filling the frame). +- Nothing prompted marking it. "Anything else near your meter?" said "Nothing marked yet" and the tester tapped + "Looks complete". +- The result says "Distance from AC units: Not everything within 3 ft 0 in of the battery was seen, so **a AC unit** + could hide there." (grammar: "an AC unit"). + +It is fair that the app has no detector. But a homeowner who just filmed their AC for 8 s is told an AC "could hide +there". An on-device detector or a "Did you see an AC unit?" nudge would help (#27 is the broader "other equipment" mark). + +### A41-2055-13 (P3, app): ground type skipped + +At 1:38.5 "What's on the ground along this wall?" (Grass / Mulch / Gravel / Concrete / Driveway / Deck / Not sure) +sat above "Anything else near your meter?". "Looks complete" was enabled with nothing chosen and the tester sent it. +`scene.ground` is empty, so "Ground under the battery" is unsure. The ground here is clearly gravel. Either require an +answer ("Not sure" is one tap) or pre-select from the camera. + +### A41-2055-14 / -15 / -16 (P3): repeats + +- "Ending the wall here leaves out N ft you walked" (2, 5, 8, 11 ft) under the strip while the tester walked out into + the yard, not along the wall (A41-1-16, A41-2015-16). +- "Slow down" at 0:54 while tilting (#26). +- "Demo rules…" in the homeowner's summary (A41-2015-19). + +### A41-2055-17 / -18 (P3, test) + +- The phone started at 12% and ended at 2%. The recording stops mid-upload at 2:43, probably the battery (inferred). + Charge before runs. +- The photos show the house number, the street name and the meter number (on a sticker on the meter). Keep this scan + out of public issues; crop or blur any frame used as evidence. + +## Timeline + +Video time; capture time = video − 6.6 s. + +| Time | Screen / event | Narration | +|---|---|---| +| 0:00–0:05 | Control Center (color filters, brightness), launch, onboarding, "Allow camera" | "third test run, color on, brightness up… allow camera" | +| 0:06–0:10 | "Find your electric meter" | "Here's the meter. This is my meter." | +| 0:10–0:12 | Tap refused: **"Step a little closer to the wall / Then aim at your meter again"** (no vertical plane) | "Step a little closer." | +| 0:13 | Second tap accepted, **estimated plane**, anchor 1.1 m behind the wall (-01) | "This is my meter." | +| 0:13–0:16 | Close-up (g1), "Which number is on your meter?" the correct 7-digit number / a wrong 6-digit number / None, picks the correct 7-digit number, "Meter number saved" | "There we go. That's the number." | +| 0:17–0:21 | "Take a step back" (g2) | "Taking a step back." | +| 0:21–1:34 | **"Tilt down to show the ground… at your meter" (g3), 73 s**; "too dark" card ×≥ 8; "Slow down" 0:54; AC condenser in view 0:54–1:02; walk into the yard and back 0:57–1:16; "leaves out N ft" | "I cannot turn on more light. I'm sorry." (0:48); "I'm just gonna look around." (0:53); "I'm just gonna end the test here." (1:30) | +| 1:34.0–1:36.5 | **7× "Can't get there"**: g3, walk left (left end −13.1 ft), walk right (right end +1.7 ft), overhead (button overlaps text), tilt ×3 | "Can't get there" ×5, "Done" | +| 1:36.5–1:38 | "That's the whole wall", "Done with this wall" | | +| 1:38.5–1:39.5 | Ground type not answered; "Anything else near your meter? Nothing marked yet"; "Looks complete" | "Done. Looks complete." | +| 1:39.5–1:42 | "Sending your measurements", "Checking your wall" (~2 s) | | +| 1:42–1:45 | "4 more views to finish"; dark card; "Show the ground out to about 14 ft" (g10); "I can't get there" | "Let me save this. Can't get there." | +| 1:45–1:46 | "Checking your wall" (~1 s) | | +| 1:46–1:48 | **Result**: "An installer will take a look / 4 ft 7 in left of your meter, 3 ft of cable"; 3D preview; banners; checks and "Still needed" scrolled | | +| 1:48–1:50 | "Share scan", 33 MB zip | "Share scan." | +| 1:50–2:43 | Share sheet, Google Drive, upload in progress; recording ends at 2% battery | Background conversation, not about the app | + +## Result as displayed (matches the server re-run) + +- **Decision:** manual_review, "An installer will take a look". Summary: "More views are needed around the best spot, + 4 ft 7 in left of the meter: 7 checks depend on areas the scan did not see. Demo rules: public values and + placeholders (pool 10 ft, drive 5 ft), not Base's." +- **Spot:** −5.90 → −3.32 ft, 2 ft 7 in wide; cable route 3 ft 4 in (± 2 ft 0 in) along the wall. +- **Banners:** "A closer spot may exist on the left of your meter. The scan didn't reach that side."; "Your electrical + panel still needs an electrician's review…" +- **Pass:** wall backing, meter working space (2 ft 1 in clear), nothing mounted above, facing gap, cable route, cable + length. +- **Unsure:** ground under the battery, gas (3 ft), AC (3 ft; "a AC unit"), driveway (5 ft), pool (10 ft), doors and + windows (3 ft), headroom. Each is marked "One more photo would settle this". +- **Still needed:** + - Ground from 13 ft 2 in left to 1 ft 9 in right, 13 ft 1 in out ("An installer will check this"). + - Overhead from 5 ft 11 in to 3 ft 4 in left, 6 ft 6 in up. + - Keep walking past the left end (13 ft 2 in left). + - Keep walking past the right end (1 ft 9 in right). +- **Solver stats:** 453 candidates, 0 pass, 393 unsure, 60 fail. + +## Scan summary + +| Item | Value | +|---|---| +| Duration, distance | 98.4 s of capture (video 0:06.6 → 1:45.0), 27.0 m walked (mostly the yard detour and back) | +| Photos | 65 kept, 1920×1440, no depth, all `tracking: normal`; ISO 1000–3200; mean luma 44–99 (median 57) | +| Guidance | 10 entries: 1 close-up, 1 step_back, 4 tilt_to_ground, 2 walk, 1 overhead gap (phone), 1 ground gap (server). Only g1 and g2 were `met` | +| Wall | One wall, `source: tap` (estimated plane), baseline −13.14 → +1.72 ft. **~1.1 m behind and ~46° off the detected house wall** | +| Ends | Both `unexplored`, from "Can't get there" on the walk cards: left −4.005 m, right +0.525 m | +| Meter | `plus_minus_ft` 0.79; meter number the correct 7-digit number (stays on the phone) | +| Ground | `ground_y_m` −1.5528 m = the higher `floor` plane (7.0 × 3.6 m). A second `floor` plane at −1.806 m (9.9 × 9.7 m) was not used, correctly | +| Marks | Meter, two ends. No objects, no ground answer | +| Coverage observed | Wall −13.1 → +1.7 ft (7.5 ft up). Ground −4 → +1.7 ft, 3–7 ft out. Facing 5.5–14 ft. All relative to the wrong wall line | +| Planes | 8: 2 horizontal `floor`, 6 vertical (3 `wall`, 3 `none`). The four near the meter agree on a normal ~(0.55, 0, 0.84) | + +## Test-ID results + +Matched by what was on screen; the tester didn't say IDs. Haptics unverified. + +| ID | Result | Note | +|---|---|---| +| O-1 | Pass | Onboarding, "Allow camera" | +| F-1 | **Fail (silent)** | The meter tap was accepted, but on a bad estimated plane (A41-2055-01) | +| F-2 | Seen | "Step a little closer" refusal, misleading copy (A41-2055-07) | +| C-1 | Pass | Close-up and meter number the correct 7-digit number | +| W-1 | **Fail** | 73 s on the first task, abandoned (A41-2055-03, -04) | +| W-2 | Pass | Photo counter flashes green | +| W-7 / W-8 | **Wrong** | Ends from "Can't get there" at −13.1 / +1.7 ft (A41-2055-06) | +| W-9 | Known | "Slow down" during tilt (#26) | +| G-1 | Not tried | The server gap was refused at once | +| G-2 | Pass, copy bug | "I can't get there" uploads; "4 more views", one shown (#39) | +| U-1 | Measured | Upload + check ≈ 2 s, then ≈ 1 s | +| R-1 | Reached, **wrong** | manual_review, spot inside the house (A41-2055-02) | +| R-2 | Not tried | 3D model shown, not dragged | +| R-3 / R-4 | **Not tested** | "See it on your wall" not tapped | + +## Suggested order of fixes (this run's view) + +1. **A41-2055-01**: sanity-check the meter tap's plane and re-fit the wall to detected planes. On a non-LiDAR phone + in low light, this bug silently wrecks the whole scan, and it can happen in daylight too. +2. **A41-2055-02**: the server shouldn't return a spot, or "pass" wall-backing and route checks, for an unwalked + `tap` wall with both ends unexplored. The app shouldn't offer the AR view for it. +3. **A41-2055-03**: a timeout or skip on the first ground task. It has blocked all three 4.1 runs, for three different + reasons. +4. **A41-2055-04**: dark-card hysteresis, keep the task and its button, offer the torch. +5. **A41-2055-05**: "Finish scan" with a confirmation; input lock between cards; confirm before two refused walks + write two ends. +6. **A41-2055-06**: pull the device log for the end positions. +7. Copy and layout: -07, -08, -09, -12. +8. Existing: #39, #26, #27. + +## Housekeeping + +- Code was read in the existing worktree `C:\dev\house-scanning-beta41` (detached at `45017ba`). The solver re-run and + the packet validator used `%TEMP%\hs-tools`; transcription used `%TEMP%\hs-whisper`. +- Nothing was committed, pushed or posted.