Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
3b25897
fix(viewer): preserve run evidence and recover result reads
SamGu-NRX Oct 13, 2026
2ab1708
docs: match review conclusions and examples to source evidence
SamGu-NRX Oct 13, 2026
d06435e
fix(checks): include viewer tests and reject experiment drift
SamGu-NRX Oct 13, 2026
d0b5444
fix(checks): test viewer dispatch and fail on project-list errors
SamGu-NRX Oct 13, 2026
aaa6e8c
fix(viewer): honor backoff through preview invalidation
SamGu-NRX Oct 13, 2026
02d6073
docs: state both conditions for checking battery clearance
SamGu-NRX Oct 13, 2026
224a457
fix(viewer): refresh invalidated results and isolate preview retries
SamGu-NRX Oct 14, 2026
bfbfb64
docs: include viewer checks in contributor guidance
SamGu-NRX Oct 14, 2026
3ac6e49
fix(viewer): separate server deadlines from polling cadence
SamGu-NRX Oct 14, 2026
8bc0729
fix(viewer): suppress unchanged invalidated preview artifacts
SamGu-NRX Oct 14, 2026
2bbfb25
docs: correct reconstruction attribution and battery cable routing
SamGu-NRX Oct 14, 2026
73ad981
fix(viewer): preserve storage error responses when cancel rejects
SamGu-NRX Oct 14, 2026
5ebf51f
fix(viewer): bind preview retries to the requested artifact
SamGu-NRX Oct 14, 2026
2e8cddd
fix(viewer): retain resource query selectors in preview identity
SamGu-NRX Oct 14, 2026
09581fa
fix(viewer): settle forced refreshes answered by retired runs
SamGu-NRX Oct 14, 2026
6c8593f
docs: explain the pinned XcodeGen prerequisite for root checks
SamGu-NRX Oct 14, 2026
33db360
fix(viewer): preserve deadlines and results on malformed replies
SamGu-NRX Oct 14, 2026
92a2735
fix(viewer): show errors after pending result refreshes
SamGu-NRX Oct 14, 2026
3e82e8c
fix(viewer): retain prompts and isolate contact retry delays
SamGu-NRX Oct 14, 2026
ce9de6d
fix(viewer): clear refresh notes when no result body remains
SamGu-NRX Oct 14, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ A user's instruction outranks a skill.

## Working in the repository

- `make check` runs every suite on the branch. `make ios`, `make server`, `make web`, `make scoring`, `make measure-lab`, `make evals`, `make recon` and `make meter-closeup` run one each.
- `make check` runs every suite on the branch. `make ios`, `make server`, `make web`, `make scoring`, `make measure-lab`, `make evals`, `make recon`, `make meter-closeup` and `make integration-viewer` run one each.
- Branch from `main`, keep one writer per branch, and open a pull request. People merge, and agents never push to `main` or merge, so a person sees every change before it lands.
- Several agents share one Mac. Exit code 137 means the system killed the process, usually for memory. Find the large allocation before rerunning, because one runaway process can freeze the whole machine.
- Put `DEVELOPMENT_TEAM` in `ios/Config/Local.xcconfig` (copy `Local.xcconfig.example`), not in Xcode's Signing & Capabilities pane. The pane writes into `project.pbxproj`, and CI fails on that drift.
Expand Down
4 changes: 3 additions & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,9 @@ The iOS UI tests skip the every-state accessibility audit on pull requests, beca

The iOS check runs as three jobs, each under its own 90-minute cap: `screenStates` runs `ScreenStatesUITests`, the every-state audit included under `full-ui`; `photoProcessing` runs an unsigned Release build and `PhotoProcessingUITests` with its own audits; and `journey` runs the package tests, the Debug build and every other UI test class. `ios/Tools/ui-test-groups.py` holds the groups' test selectors; `python3 ios/Tools/ui-test-groups.py check` lists the UI tests from source and fails unless the groups run each exactly once. Run it after adding or renaming a UI test class. Each job keeps its result bundle as the artifact `ui-test-results-<group>`, with `SNAPSHOT.txt` saying whether the copy is complete; a job the cap cancelled can leave it incomplete.

`make check` runs the server, web and iOS suites, then `make scoring`, `make measure-lab`, `make evals`, `make recon` and `make meter-closeup` for each of those folders the branch has. They need uv, Node 24 with pnpm, and Xcode 26 or newer; each directory's README has details. No check is required by branch rules yet. Don't call one required until the rules require its status.
`make check` runs the server, web and iOS suites, then `make scoring`, `make measure-lab`, `make evals`, `make recon`, `make meter-closeup` and `make integration-viewer` for each of those suites the branch has. They need uv, Node 24 with pnpm, Xcode 26 or newer, and XcodeGen 2.46.0. Measure Lab regenerates its committed project before testing, so XcodeGen must match CI's pinned version to avoid project drift. The viewer's browser regression runs when Chrome or Chromium is available and skips otherwise; each directory's README has details. The viewer has no GitHub CI job yet. No check is required by branch rules yet. Don't call one required until the rules require its status.

To install the pinned XcodeGen, download [XcodeGen 2.46.0's xcodegen.zip](https://github.com/yonaskolb/XcodeGen/releases/download/2.46.0/xcodegen.zip). Run `shasum -a 256 xcodegen.zip` and compare it with `XCODEGEN_SHA256` in [the Measure Lab workflow](.github/workflows/measure-lab.yml). Unzip it and add the extracted `xcodegen/bin` directory to your `PATH`. `xcodegen --version` must print `Version: 2.46.0`. To use the binary without changing `PATH`, run `make check XCODEGEN=/absolute/path/to/xcodegen/bin/xcodegen`.

Keep workflows that run pull-request code away from production credentials and destructive external systems. A green CI run is evidence for the checks it ran, not proof that a capture works on a real house.

Expand Down
23 changes: 18 additions & 5 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Each suite target runs the commands of its CI job in .github/workflows/.
# Suite targets follow their CI jobs where one exists. The viewer has no CI job yet.
# `make check` runs server, web and ios, then scoring, measure-lab, evals, recon
# and meter-closeup where the branch has them.
# meter-closeup and the integration viewer where the branch has them.
# tests/makefile.sh checks this dispatch with stub suites.

XCODEGEN ?= xcodegen
Expand All @@ -15,20 +15,22 @@ MEASURE_LAB := experiments/measure-lab/Makefile
EVALS := experiments/evals/Makefile
RECON := recon/Makefile
METER_CLOSEUP := experiments/meter-closeup/Makefile
INTEGRATION_VIEWER := tools/integration-viewer/package.json

# $(call present,FILE,TARGET) is TARGET when FILE exists, otherwise nothing.
present = $(if $(wildcard $(1)),$(2))
# $(call require,FILE,PR): stop before any command runs when the suite's file is missing.
require = @test -f $(1) || { echo "make $@: $(1) is missing; the suite arrives with pull request $(2)." >&2; exit 1; }

.PHONY: check server web ios ios-project smoke scoring measure-lab evals recon meter-closeup
.PHONY: check server web ios ios-project smoke scoring measure-lab evals recon meter-closeup integration-viewer

check: server web ios \
$(call present,$(SCORING),scoring) \
$(call present,$(MEASURE_LAB),measure-lab) \
$(call present,$(EVALS),evals) \
$(call present,$(RECON),recon) \
$(call present,$(METER_CLOSEUP),meter-closeup)
$(call present,$(METER_CLOSEUP),meter-closeup) \
$(call present,$(INTEGRATION_VIEWER),integration-viewer)
Comment thread
SamGu-NRX marked this conversation as resolved.

server:
cd server && uv sync --locked
Expand Down Expand Up @@ -64,9 +66,14 @@ scoring:
cd experiments/scoring && uv run ruff format --check .
cd experiments/scoring && uv run pytest -q

# The Geometry package's tests, then the app build.
# Regenerate and check the committed project before Geometry tests and the app build.
measure-lab:
$(call require,$(MEASURE_LAB),#7)
$(MAKE) -C experiments/measure-lab project XCODEGEN="$(XCODEGEN)"
Comment thread
SamGu-NRX marked this conversation as resolved.
git diff --exit-code -- experiments/measure-lab/MeasureLab.xcodeproj
# A failed listing must not look like a clean generated project.
@untracked="$$(git ls-files --others --exclude-standard -- experiments/measure-lab/MeasureLab.xcodeproj)" || exit 1; \
test -z "$$untracked" || { echo "MeasureLab.xcodeproj contains uncommitted generated files." >&2; exit 1; }
$(MAKE) -C experiments/measure-lab check

# The folder's `make test` lints and tests but leaves out the `uv sync --locked` that
Expand All @@ -80,11 +87,17 @@ recon:
$(call require,$(RECON),#20)
$(MAKE) -C recon test

# Locked sync matches the metric CI job and rejects stale locks before uv run can rewrite them.
# Lint and unit tests only. The separate leak scan needs a private key and must not run here.
meter-closeup:
$(call require,$(METER_CLOSEUP),#16)
cd experiments/meter-closeup && uv sync --locked
$(MAKE) -C experiments/meter-closeup check

integration-viewer:
$(call require,$(INTEGRATION_VIEWER),#124)
cd tools/integration-viewer && node --test "test/*.test.js"

# Posts each scene in server/examples to a placement server and prints its decision:
# make smoke URL=https://house-scanning-server.vercel.app [KEY_FILE=server/.env.private.local]
smoke:
Expand Down
11 changes: 6 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -113,14 +113,15 @@ The battery measures 31 × 22 × 39.5 in, a bit bigger than a dishwasher. It sta
| Cable route | Can't cross a door, a garage or a gap in the wall | Demo choice |
| Driveway | At least 5 ft away | Placeholder, no public value |
| Pool | At least 10 ft away | Placeholder, no public value |
| Existing battery, when the scene marks one or its configured clearance exceeds the AC clearance | At least 3 ft away with shipped rules; the cable detours over it | Base's help page; IRC 2021 R328, cited in `server/rules.yaml` |

Each check comes back PASS, FAIL or UNSURE, along with what the server measured, how far off that measurement could be and a reason in plain English. NEC is the National Electrical Code and IRC is the International Residential Code, the two building codes behind several of these rules. The numbers themselves live in `server/rules.yaml` on PR #11's branch, each one next to its source, and [docs/04](docs/04-prior-art-and-codes.md) has the full citations.
Each check comes back PASS, FAIL or UNSURE, along with what the server measured, how far off that measurement could be and a reason in plain English. NEC is the National Electrical Code and IRC is the International Residential Code, the two building codes behind several of these rules. The first 13 checks run at each spot. `battery_clearance` also runs when the scene marks an existing battery or the configured battery clearance exceeds the AC clearance. The shipped public rules give both a 3 ft limit, so a scene without a battery omits that extra check. The numbers themselves live in `server/rules.yaml`, each one next to its source, and [docs/04](docs/04-prior-art-and-codes.md) has the full citations.

The server then gives one of three answers for the whole scan. It says `pass` when a spot passes every check. It says `reject` only when every spot within cable reach fails and the app knows where the wall ends on both sides. Anything in between is `manual_review`, and it goes to a person.

### One request, start to finish

Here's what that looks like in practice. We took the example scene from the server's tests in PR #11 and sent it to the demo server. The scene is made up. We wrote it by hand, with a gas meter, a window and an AC unit spread over two walls. The reply is real, though, trimmed to the parts worth reading.
Here's what that looks like in practice. We took the example scene from the server's tests in PR #11 and sent it to the demo server. The scene is made up. We wrote it by hand, with a gas meter, a window and an AC unit spread over two walls. The reply is real, though, trimmed to the parts worth reading. The abbreviated scene below omits fields needed to reproduce it. Use [the complete test fixture](server/tests/fixtures/example-scene.json) for a runnable request.

The server stopped short of placing the battery itself. Its best spot was 9 ft 11 in left of the meter. From there, the AC unit around the corner measured 4 ft 1 in away, against a 3 ft rule. That sounds like a pass until you see the margin of error, which is give or take 3 ft 10 in. It's that wide because the AC unit sits far along the wall from the meter, where tracking error piles up the most. With an error that wide, the result is too close to call. So the answer is a manual review, plus a request to keep walking past the left end of the scan, because a spot within cable reach might be there.

Expand Down Expand Up @@ -174,7 +175,7 @@ The server stopped short of placing the battery itself. Its best spot was 9 ft 1
{
"id": "ac_clearance", "outcome": "unsure",
"measured_ft": 4.08, "plus_minus_ft": 3.8, "threshold_ft": 3.0, "comparison": "at_least",
"reason": "objects[3] ac is 4 ft 1 in (± 3 ft 10 in) from the battery against a 3 ft 0 in rule: too close to call."
"reason": "The ac is 4 ft 1 in (± 3 ft 10 in) from the battery against a 3 ft 0 in rule: too close to call."
}
],
"missing_evidence": [
Expand Down Expand Up @@ -277,7 +278,7 @@ A few terms first, in case they're new to you. Tracking drift is how far the pho
| Tracking drift, older iPhone | Two to three times over that allowance | ADVIO, iPhone 6s, PR #12 |
| Learned depth on its own | Scale 4 to 12% off, which puts walls about 20 in out (p90) | ETH3D, PR #12 |
| Learned depth, rescaled with the phone's poses | Walls within about 5 in (p90), or 2.8 in with exact poses. Edges stay at 8 in or worse | ETH3D, PR #12 |
| Reconstruction worker | Walls within 2.1 in (p90) with a laser scan standing in for LiDAR, and 2.8 in from photos only | ETH3D, PR #20 |
| Reconstruction worker | Wall pair-error p90 of 1.8 in with a laser scan standing in for LiDAR, and 1.5 in from photos only. Both fit stretches of the same wall, with different lengths | ETH3D, PR #20 |
| Reading the meter number | Read in full on 71 of 73 photos, but the right line on only 21 of 75. A list of three candidates held it on 27 of 34 held-out photos | Photos of real meters, PR #16 |
| First run on our phone | Both wall ends landed at the meter, so the server placed no spot. Two of five features came within 4 in of the tape | TestFlight build, PR #23 |

Expand All @@ -287,7 +288,7 @@ Here's what we know is either missing or broken:

- **Most of it isn't merged.** Guided capture is in PR #10, the rules engine in PR #11, reconstruction in PR #20 and the packet spec in PR #22. All four still live on their own branches.
- **Our first real phone run placed nothing.** The app put both ends of the wall right at the meter, which left the server a wall with no length to search (PR #23).
- **We haven't settled on how to build the 3D model.** A depth model on its own puts walls about 20 in off. Correcting its scale with the phone's poses brings that down to about 5 in, but edges stay 8 in off or worse, and the clearance rules measure from edges. With a laser scan standing in for LiDAR, walls land within 2.1 in.
- **We haven't settled on how to build the 3D model.** A depth model on its own puts walls about 20 in off. Correcting its scale with the phone's poses brings that down to about 5 in, but edges stay 8 in off or worse, and the clearance rules measure from edges. With a laser scan standing in for LiDAR, the worker reports wall pair-error p90 of 1.8 in, as recorded in [the reconstruction handoff](recon/HANDOFF.md).
- **Our best tracking result came from a phone with LiDAR.** An iPhone 14 Pro Max stayed inside the error allowance, but it has LiDAR, and most homeowners' phones don't. An older iPhone 6s ran two to three times over.
- **Nobody has taken on hidden walls yet.** A bush or a trash can in front of the wall can hide what's behind it, and on phones without LiDAR, nothing checks for that.
- **Two rules use placeholder numbers.** We couldn't find a public value for how far a battery should sit from a pool or a driveway, so for now they're 10 ft and 5 ft.
Expand Down
2 changes: 1 addition & 1 deletion docs/00-overview.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ These results come from public datasets with laser-scanned or surveyed ground tr

- **Tracking.** A current iPhone stayed inside the server's error allowance. On the MARViN dataset, an iPhone 14 Pro Max had p90 position error of 8.6, 13.4 and 18.5 in after 10, 20 and 30 ft, against an allowance of 19.2, 38.4 and 57.6 in. That phone has LiDAR, and MARViN's reference may be scaled to its tracking. A 2018 iPhone 6s on ADVIO ran two to three times over, partly from noise in that dataset's reference.
- **Learned depth.** A model's own scale is 4 to 12% off. Rescaled with the phone's poses, walls come within about 5 in at p90 at a realistic 2% pose error, and within 2.8 in with exact poses. Edges stay at 8 in or worse, and clearances are measured from edges. MapAnything's scale shifted with input resolution.
- **Reconstruction worker.** On the ETH3D building, wall p90 is 2.1 in with a laser scan standing in for LiDAR, and 2.8 in from photos only. The two runs measured different walls.
- **Reconstruction worker.** On the ETH3D building, wall pair-error p90 is 1.8 in with a laser scan standing in for LiDAR, and 1.5 in from photos only. Both fit stretches of the same wall, with different lengths. The photos-only drop from 2.8 to 1.5 in combines the scoring fix with a different fitted stretch; see `recon/HANDOFF.md`.
- **Coverage.** The app's first coverage map claimed 1.1 ft of a 19.3 ft wall that no photo saw. Requiring several view angles removed the false claim but also discarded 6 to 8.5 ft of good wall. A depth test on LiDAR phones removed it cleanly. Checked against depth, the reconstruction worker claims at most about 0.46 ft of unseen wall.
- **Meter reading.** Apple's on-device text recognition read the full meter number on 71 of 73 real photos. It picked the right line on only 21 of 75, because nameplates carry several numbers. A barcode held the number on 20 of the 24 photos that had one. A list of three candidates held it on about 4 in 5 held-out photos (27 of 34). So the app offers three candidates to tap, barcode match first, and asks for a retake when the photo is blurry or the number too small.
- **Panel labels.** Not tested. A search found 7 usable open photos of US panels, and the test needs 40.
Expand Down
2 changes: 1 addition & 1 deletion docs/app-review/product/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,4 +75,4 @@ and Open questions and verification, ending with the commit it was verified agai
| [screens/uploading.md](screens/uploading.md) | Sending the scan and waiting for the result, offline and failure | drafted; checked in the Simulator |
| [screens/result.md](screens/result.md) | The result, its checks and the 3D and AR views | drafted; checked in the Simulator |
| [verification.md](verification.md) | Checks against the running app and their results | current pass at `a39d0a5` |
| [bug-triage.md](bug-triage.md) | Every suspected defect, deduplicated | 16 entries, 2 resolved |
| [bug-triage.md](bug-triage.md) | Every suspected defect, deduplicated | 16 entries, 3 resolved |
3 changes: 2 additions & 1 deletion docs/app-review/product/bug-triage.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,9 @@ source and not run. Every blocker and major entry was re-read in the source by a
**B-03. Failures other than an unsupported phone never reach the screen.** Only the failure screen
shows a failure, and the app switches to it only for an unsupported phone
(`HouseScan/Runtime/ScanEngine.swift:91-93`). A denied camera, a failed session or an unreadable
replay set the failure without changing screen (lines 105, 379, 381). The homeowner stays on
replay set the failure without changing screen (lines 105, 379, 381). With a denied camera or failed live session, the homeowner stays on
"Find your electric meter" with no camera and taps do nothing; "Open Settings" is unreachable.
An unreadable replay can fail while still on onboarding, because it loads in the background.
Seen with `-replay /nonexistent`: the app logged "replay unreadable: cannot read
/nonexistent/session.json" and stayed on onboarding (report
`sim/20260926-044618-5520229-5520229d-preview-bad-replay`).
Expand Down
Loading