Repository navigation
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
|
Important Review skippedToo many files! This PR contains 330 files, which is 230 over the limit of 100. To get a review, reduce the PR to 100 files or fewer by splitting it into smaller PRs or changing its base branch. Upgrade to a paid plan to raise the limit. Usage-priced reviews support at most 300 files. ⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Advanced Run ID: ⛔ Files ignored due to path filters (123)
📒 Files selected for processing (330)
You can disable this status message by setting the Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
The ground under the wall was looked up once, when the meter was tapped, and fell back to camera height minus 1.4 m for the rest of the scan. The lookup now re-runs whenever ARKit's horizontal planes change, replacing the guess (or an earlier plane) and republishing the wall. While the ground is still a guess, scene.json carries plus_minus_ft of 0.3 m on the meter and every object: the guess's error enters every height, and the schema has no other place to say the ground was estimated. A replay's recorded wall counts as measured; an assumed one does not. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
After 20 s of relocalizing the engine threw away the scan in every phase, including an upload in flight (its answer was then dropped) and the result. The spatial reset now runs only from findMeter through gapRequest. Uploading and the result keep the scan and the answer; the AR result already hides its overlay while tracking is not normal. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
"Capture it now" on a past_end item cleared that end and started a gap step, but markWallEnd only worked in the walk, so the end could never be marked again. During a past_end request, markWallEnd now accepts that side; answering the end question settles the request and goes on to the upload. If the request closes by coverage instead, the end stays cleared and exports as unexplored. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- Sweep zones: start_ft is a range of battery left edges, so a zone runs to the last start plus the spot's width. - Keyframe writes in flight are counted per scan generation; resetAll no longer zeroes a counter that a finishing write then drove negative. - Entering the close-up clears a pending shot from an earlier visit. - A refined meter anchor or measured ground moves the coverage cells and marked ends with the meter (whole cells; the sub-cell remainder carries over) and re-projects every marked feature from its tapped world points. - An UNSURE check without unsure_cause needs a person. - Missing evidence is capturable only when a gap request can be built from it. - The unseen side ignores an unexplored end beyond cable reach. - A keyframe write that finishes after its world frame was discarded is dropped. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…calization, past-end, fence depth, guidance arrow Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
… one Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…time The planner's dwell ran on frame timestamps. On a replay at 3x speed that let the card change every second or so (walk left, tilt down, walk left), which the accessibility audit caught mid-transition and the verify lane's I6 check reads on the wall clock. Live, both clocks agree. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ks inside and scalable CI's Simulator showed a system banner over the photo count during an audit, and once the accessibility tree changed while an audit read it. The screen-state audit now fails on issues found again six seconds later, and a failed snapshot is retried once in both UI tests. On the ADVIO walk the meter sits at the strip's end, and its mark hung half outside the strip, which the audit reported as clipped text. Marks now stay inside the strip, and their glyphs follow the text size up to 16 pt instead of fixed 9 and 10 pt. The walk's distance hint counts down in 5 ft steps above 5 ft, so the card no longer changes every stride. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
|
Note Review skipped Codemetrics is installed on this repository, but it hasn't been added to your team yet. To enable AI code reviews for this repo, go to your dashboard and add house-scanning to your team's repositories. |
… picker Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…es to replace Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
… reader Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
updateWall only shifted cells along s, so after the ground height was corrected the export claimed wall and ground rows sampled in the air. The map now keeps the camera of every normally tracked keyframe and replays them against the new frame, keeping skipped cells and ends. refineGround republishes the strip after the rebuild. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…vives tap geometry
POST {serverURL}/v1/placements now carries scene.json as application/json;
the solver reads no photos, so keyframes stay on the phone and scan.zip is
still written for replay and debugging but never uploaded or awaited.
Network failures and 5xx become failed (Try again); 4xx, an unreadable
answer and a scene that can't be exported become rejected with homeowner
wording, the technical detail going to the log. backToReview returns a
rejected upload to the feature review with the scan kept. The export clamps
opening heights, moves ground taps behind the wall line to just in front of
it, and drops a zero-length driveway edge instead of throwing.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…s as strings The app never reads them, so a new server value there should not fail every decode. Decision, outcome and unsure_cause stay strict. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
HouseScanServerURL = $(HOUSESCAN_SERVER_URL), set in Shared.xcconfig to the deployed server. -serverURL overrides it and -sampleResult still forces the bundled sample, which the full-flow UI test passes so it stays offline. An empty or non-http(s) value means no server and falls back to the sample. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
scene.schema.json now closes attrs to operable and well; the export only writes operable, and a test checks another key is rejected. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A denied camera, a failed AR session and an unreadable replay set state.failure but left the flow where it was; only an unsupported phone reached .unsupported. All four now go through one fail(_:) that sets the failure and the phase. Checked with -replay /nonexistent: STATE=onboarding, then STATE=unsupported. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Every new KeyframeStore (at launch, and on Start over) deletes the other folders under Caches/Scans off the main actor. Before, each Start over left its previous Caches/Scans/<UUID> behind and nothing ever removed one. Checked on the Simulator: 30 folders before launch, 1 after. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Coverage marks what the camera pointed at. The server does not read images, so nothing downstream corrects a bush or bin counted as seen. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A failed try needed one problem lasting 4 s with no good frame between, so frames alternating sharp and blurry never produced a photo or a failed try, and "Can't get a clear shot" never appeared. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…otos stay on the phone The second page said only "Your phone takes the photos". Uploads now carry the wall's measurements, so the page says what leaves the phone and that the photos are deleted on Start over, before the camera is asked for. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…e recording Both failures showed the engine's message, which is system error text (ARKit's localizedDescription, or String(describing:) of the replay loader's error). They now say what happened in the homeowner's words, and Start over, the only way on, is the screen's primary button. Open Settings uses the primary style too, so it gets the pressed state a Link lacked. The demo can open on either failure (-uiDemoFailure sessionFailed|replayUnreadable). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
# Conflicts: # ios/HouseScanUITests/ScreenStatesUITests.swift
|
Codex Review: Didn't find any major issues. You're on a roll. Reviewed commit: ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
If Codex has suggestions, it will comment; otherwise it will react with 👍. Codex can also answer questions or update the PR. Try commenting "@codex address that feedback". |
The photo was chosen with its raw camera against the corrected wall, and the outline drawn through the raw pose: after a 0.30 m anchor correction the outline sat tens of pixels off the spot while the whole-area check passed. Each candidate is now judged with its camera corrected by the anchor corrections applied since it was taken (SpotPhotoCandidate.camera(correctedBy:)), and the choice carries that camera for the outline. The photo and camera are fixed together after the decode, and judged and drawn again whenever the wall moves while the question is up (refreshSpotPhoto, from publishWall); a photo that no longer shows the whole area turns the question into leaving the area out. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The planner joined ground spans to the 125-entry band budget, but the export joins ground to 124 on a chain with one corner, to leave room for cutting it there. At the boundary the export merged two neighbours (2.5 ft and 3 ft) that the planner kept, and a 3 ft request settled on the phone while the scene sent 2.5 ft (caretaker review, GapPlanner P2). SceneExport.groundEntries is the one normalization; the export and the planner's settlement both read ground through it. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 77d820bb69
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
The LiDAR replay at 9b4c57d failed contrast on action.cannotAccess. Its recording shows "Can't get there" and "Can't see past it" drawn over each other half-transparent as the walk moved to the see-behind step: the reply stayed one button, so its new title cross-faded inside the card's instruction animation. It now takes a new identity per title and swaps at once, as the message above it does. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A scan's first bundle write creates scan.zip.partial; if the app died before scan.zip existed, the next launch counted that folder as completed and newest, so it took one of the two slots and evicted a real completed scan (Greptile 4115191959). Now only scan.zip marks and dates a completed scan; a folder with both counts by its scan.zip. A partial-only folder is kept apart, uncounted: the newest one, while it is newer than every completed scan, so its photos last until the next scan completes. Older ones go. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
scene.json, the mesh measurement and the packet read the live wall, ground, marks and ARKit's current mesh at different moments, so a correction or ground change between reads made them describe different walls. - UploadSnapshot (ScanEngine+Snapshot.swift) is taken on the main actor in ingest, right after a frame's anchor correction and ground update. It holds the coverage map, ground, marks as world points, raw keyframes with the pose corrections, stills, end kinds, spot ground patches, and that frame's mesh and planes. - LiveCapture reads the mesh and planes from the delivered ARFrame's own anchors when asked, stamped with its timestamp. The snapshot keeps them only when that time is the frame's, and goes without them if no such frame arrives within 2 s. There is no more currentFrame read and no retry loop. - sceneJSON, packetInputs and the mesh measurement take the snapshot; nothing after it reads live state. - Pending taps are kept as world points and moved by corrections, so a mark's heights come from the snapshot's ground. - spatialRevision counts corrections, ground changes, new walls and followed corners. A stale answer is dropped and the scan sent once more; a second stale answer is not shown and the upload fails with "still adjusting, try again" (AnswerFreshness). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
680e4b7: only a real scan.zip makes a completed scan; a partial-only folder takes no slot. 4ef41b3: one upload snapshot from a frame-stamped input; the mesh only from the matching frame; stale answers resent once; mark heights from world points against the snapshot's ground. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Start over sits under the result card's collapsed Details (ResultScreen), as ScreenStatesUITests already reaches it; 1c496fe stopped at the result but still tapped a hidden button. The test also lets the spot question go on. Found by the #10 caretaker from the b33e214 run. Not rerun. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ply title Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 4f1fb8ebec
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| // A fence's feet on two pieces of the wall would be sent as one depth that misses how | ||
| // close its line comes to the wall by the corner: refused, and asked for per side. | ||
| if marking.kind == .fence, let first = pendingTaps.first, !wall.onSamePiece(first, wall.world(hit)) { | ||
| marking.refusal = .fenceAcrossCorner |
There was a problem hiding this comment.
Reject openings whose taps cross a wall corner
When the homeowner places a door or window's two taps on different pieces of a followed corner, this same-piece validation runs only for fences, so the opening is accepted with one continuous span. SceneExport then assigns that span a single wall_id, and the result renderer likewise draws the entire opening on the midpoint's wall, misplacing or losing part of the marked obstacle for clearance checks. Apply the same corner validation to two-tap wall openings, or split them into per-wall features.
Useful? React with 👍 / 👎.
…ey measure A gas meter or AC unit was one tap, exported as a 0.3 m square standing 0.3 m off the wall: a guessed width and depth. - Gas meter: two taps where it meets the wall, bottom-left then top-right, as a window or #52's box. Exported as a wall object with its span, bottom_ft and top_ft and no footprint, since a wall-plane tap says nothing about how far it stands out. The server measures to its stretch of wall line. - AC unit: two taps on the ground where its front meets it (a ray aimed at a unit meets the ground in front of the wall, #163). Exported with its span and a footprint from the wall line to that tapped front edge, so its depth is measured and the space behind it counts as taken. Height is not measured and is left out. - Refused and marked again: taps closer than 1 cm (SceneExport.minObjectSize), a single tap, and AC corners on two pieces of the wall. - The packet manifest (1.1) gives these kinds one point, so each mark carries the middle of its two corners; the packet's scene.json has the span and footprint. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A gas meter takes two taps on the wall (span and heights, no footprint); an AC unit two taps on the ground at its front (footprint to the wall line, no heights). The old 0.3 m square is gone, and a one-tap or cross-corner mark is refused, never exported as a sized object. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…lt and AR
The server's answer was checked against the spatial revision only when it
arrived. A ground plane revoked or refined while the spot check is open
raised the revision, but finishing on a path that adds no patch ("It's
clear" then "Not sure") still went straight to the result with the old
answer.
Every step that would present the accepted answer as current (the spot
check, the result after it, opening and closing the AR view) now asks
AnswerFreshness.step: an unchanged revision shows it; a changed one drops
the answer and sends the scan again from a fresh snapshot, once, then shows
the stale-answer failure, as at the POST. A step that changes the scan
uploads anyway.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
An accepted answer is shown only while the spatial revision it was built on still holds; otherwise the scan is sent again once, then fails as stale. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
|
@codex review |
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
…ft are rounding - make sim-app at c820cf1 on A4EE0B20, synthetic replay, hosted public server: 17 states, a server gap request answered "can't get there", three spot checks, the result and AR. S4-2 is met there and S4-1 partial (the real ADVIO replay is blocked by its data). - The scan's less-coverage resend flagged wall_equipment_above for a 0.0037 ft sliver past the seen wall. The server's missing() drops 1D gaps under 0.01 ft (COVERAGE_TOLERANCE_FT); the wall, route, facing and overhead checks now do too. Tested at 0.0037 and 0.011 ft. e2e at 3baa338 with that scan: 49 of 50, the ETH3D server issue. #11 moved to 6c7bea1 during the run, so S2-6 and S2-8 read stale until the turn-2 re-pin. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
|
@greptileai review Please review the current head |
| heading | ||
| featureList | ||
| addSomething |
There was a problem hiding this comment.
Wall-wide ground question is missing. The review shows the feature list and “Looks complete” without asking what is on the ground along the wall. Ground type is asked only later for a server-selected spot, and its answer covers only that footprint. If the server returns no spot, or no kept photo shows the whole spot-check area, the homeowner cannot provide a ground type, leaving the ground-surface check unsure even when they know the answer.
Prompt To Fix With AI
This is a comment left during a code review.
Path: ios/HouseScan/UI/Screens/MarkFeaturesScreen.swift
Line: 54-56
Comment:
**Wall-wide ground question is missing.** The review shows the feature list and “Looks complete” without asking what is on the ground along the wall. Ground type is asked only later for a server-selected spot, and its answer covers only that footprint. If the server returns no spot, or no kept photo shows the whole spot-check area, the homeowner cannot provide a ground type, leaving the ground-surface check unsure even when they know the answer.
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.There was a problem hiding this comment.
Caretaker check at c820cf1: not a defect in the code. The wall-wide ground question was removed on purpose after Codex 4114010727 showed one answer could label lawn as concrete (Sam, 4114020413 and 4114023377). With no spot, ScanEngine+Confirm.swift:94-100 shows the result without a ground question, and SpotConfirmation exports only answered footprints. The scene contract makes ground optional, and an unrecorded surface stays UNSURE, which is the intended outcome for unseen ground.
What is stale: the PR body and the header comment at MarkFeaturesScreen.swift:3 still describe a wall-wide question. That is description drift, recorded for #10's owner; #10 is frozen, so no change from here.
There was a problem hiding this comment.
You're right — this is not a defect in MarkFeaturesScreen or the export flow. The wall-wide ground question was intentionally removed because a single answer could overgeneralize across the wall, and leaving unseen ground as UNSURE is the intended contract behavior. The stale parts are the PR description and the screen's header comment, so I’m recording this as description drift for the frozen PR rather than requesting a code change.
| types: [opened, synchronize, reopened, ready_for_review, labeled] | ||
| paths: | ||
| - "ios/**" | ||
| - "!ios/**/*.md" | ||
| - ".github/workflows/ios.yml" |
There was a problem hiding this comment.
Server schema changes skip drift check. The new iOS contract comparison runs only when an iOS non-Markdown file or this workflow changes. A pull request that changes only a server schema will not trigger it, so an incompatible vendored iOS schema can go undetected by this check. Include the server contract paths in the workflow triggers.
Prompt To Fix With AI
This is a comment left during a code review.
Path: .github/workflows/ios.yml
Line: 13-17
Comment:
**Server schema changes skip drift check.** The new iOS contract comparison runs only when an iOS non-Markdown file or this workflow changes. A pull request that changes only a server schema will not trigger it, so an incompatible vendored iOS schema can go undetected by this check. Include the server contract paths in the workflow triggers.
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.There was a problem hiding this comment.
Caretaker check at c820cf1: confirmed, and already true on main. A ready PR that changes only server/schemas/*.schema.json matches none of ios.yml:14-17, and server.yml runs only lint and pytest, so vendoredCopiesMatchServer never runs. Adding server/schemas/** and server/tests/fixtures/example-scene.json to both path lists closes the gap, but it also puts a 45-minute macOS job on every server-schema PR. That cost trade-off belongs to #10's owner, and #10 is frozen, so it is recorded for them rather than changed here. A cheaper alternative is a separate Linux job that only diffs the vendored copies.
There was a problem hiding this comment.
That’s a fair resolution. The finding remains valid, but since #10 is frozen, I won’t recommend changing this workflow here. Recording the two concrete options for the owner is sufficient: add the server contract paths and accept the macOS cost, or add a cheaper standalone Linux vendored-schema drift job.
* Sync c3-vlm breadth work from the Project store (result file C3-vlm-baseline.md) * Sync c3-vlm breadth work from the Project store (result file C3-vlm-baseline.md)
What changed
The iOS app runs the homeowner's whole scan, from finding the electric meter to seeing where a Base Power battery could go. It sends the server the measurements it needs to decide, and says what it didn't see.
Meter. The homeowner points at the meter and taps. The scan anchors on the wall surface there: a plane ARKit detected, or ARKit's estimated vertical plane with a wider meter error in the export. The close-up photo takes itself once the picture has been sharp, lit and centred for 0.6 s. Vision's accurate mode reads the meter number, with the focus and size checks from the close-up eval (
experiments/meter-closeupon t3/meter-closeup). Then the app asks "Which number is on your meter?" with up to three candidates, barcode-confirmed first, and "None of these". It never fills the number in. A close-up that passes the focus check also counts as one camera position toward coverage.Wall walk. The homeowner walks left, then right, along the wall. The phone keeps a photo every 0.5 m or 15° of turn, or when the frame shows wall it hasn't seen. It keeps photos only while tracking is normal and the picture is sharp.
walls[].Marking. They can pin a gas meter, AC unit, door, window, driveway edge or fence as they pass. Marks past an end still export, because the server places them on the wall's continued line and decides which it can use.
The ground question (new). The feature review opens with "What's on the ground along this wall?": grass, mulch, gravel, concrete, driveway, deck, or "Not sure". The camera can't tell mulch from soil, and without a ground type every real scan ended with the server's "Ground under the battery" check unsure. An answer is exported as a
groundpatch of that type covering only the ground the coverage saw. "Not sure" sends no patch. The question doesn't block "Looks complete".What the server needs.
coverage.observedfollows "What settles each check" inserver/README.md(t3/server at 9e42970):out_ft, the height up the wall the phone saw from two positions;source;Each wall says how its line was found in
walls[].source. A wall through a plane ARKit detected isplane, and a tap on an estimated plane or a replayed wall istap. Nothing yet builds a wall line from the LiDAR mesh. Ground entries are split at corners, and every span and reach rounds inward, so no entry claims ground the camera didn't sample (see Why it works this way).Depth on LiDAR phones. The session runs scene depth and mesh reconstruction. A cell counts only where depth at its projection matches its distance. A nearer reading marks it hidden (a bush or bin in front of the wall), and a later view without depth can't clear that. The walk then asks the homeowner to look past it.
One more view. After the walk, the phone asks for the most useful stretch it hasn't seen from two positions, in one sentence, and moves on once the camera has covered it. It does the same for evidence the server lists in
missing_evidence, including a wall seen too low: those requests carry the height the view must exceed. "I can't get there" leaves the gap for an installer.Result. The phone posts
scene.jsontoPOST /v1/placementson the hosted server (Config/Shared.xcconfig) and shows the answer two ways. The first is a 3D model of the wall with the battery, the cable and the clearances. The second is the battery on the live camera, placed on the meter's anchor, where people and objects in front of it hide it. The cable now bends at every corner of the wall chain it passes, where before it was projected onto the battery's own wall. Unknownmissing_evidencekinds from a newer server go to installer review; the whole answer no longer fails on them.Getting a scan off the phone. "Share scan" shares a capture packet 1.1 (S4's spec,
packet/README.mdon t3/packet at d5439cf). New since 1.0:planeswithboundary_m;session.device.mesh_classification_enabled;depth_framesbetween photos at 2 Hz. They are written to disk as they arrive, stop at 300 frames, and are recorded only during the close-up, the walk and "one more view".estimateddepth with sigma.Photos leave the phone only through this share sheet, and no location or heading is recorded.
The capture logic is a Swift package,
ios/HouseScanKit. It imports only Foundation, simd, CryptoKit (packet hashes) and ImageIO, so its 366 tests run on macOS.HouseScan/Runtimefeeds it ARKit or a replay and runs the flow;HouseScan/UIdraws the screens.ScanContract.swiftis the whole interface between the two.For the Simulator (contract C4):
-replay <folder>plays a Measure Lab v2 session instead of the camera.-autopilotdrives every step, including a gap request, and answers the ground question with mulch.-serverURL <url>picks the server;-sampleResultanswers with a bundled sample that every screen labels as an example.-uiDemo…arguments open single states.-uiDemoGroundQuestionis new.Each screen change is logged as
STATE=<name>.ios/README.mdlists every argument and the steps to run on an iPhone.Aiden's PRs into this branch are part of it:
#37, #47, #48 and #51 were merged before their reviews cleared. Their open findings are listed under Not done. #49, #52, #53, #55, #56 and #57 are open against this branch.
Why it works this way
out_ftexceeds that check's height: 6.5 ft for gas and openings, 3.29 ft for the battery's backing, 1 ft for the cable.wallWalkHeight). That is the most a view from the 2 m stand-off can show while it also shows the ground in front of the wall; the long-side half angle of a phone's image is about 30° to 31°.wallCaptureHeight), so a tilt-up can show up to 7.5 ft.ground_surfaceunsure.band_polygonincludes the sector outside it, and no camera sample lies there. The export splits every ground entry at the chain's corners after merging.sourcesays how the line was found, and the phone subtracts that source's error. The server gives a detected-plane wall 0.75 ft of default error and a tap 0.3 ft. The walked clearance the phone reports subtracts the larger error of the two wall pieces at each cell. The phone mirrors those defaults fromrules.yamlinServerErrorDefaults.swift; they go stale if the rules change.WalkedEnd). Unseen stretches in between stay unseen, and the server asks for them. A photo taken past an unexplored end that sees nothing between the ends isn't kept, and the card says so.UI changes
New since the last description, all from the scripted demo engine at dd44fc3. The ground question is new. The number picker shipped earlier but had no capture here. These three images still need uploading to GitHub from a signed-in browser. Until then they are local files on the build Mac:
/tmp/hs-pr-shots/19-ground-question.png/tmp/hs-pr-shots/20-ground-answered.png/tmp/hs-pr-shots/21-number-picker.pngThe rest are from earlier commits and still show the current screens. The replay rows are the real engine on the synthetic replay at beede15 with the bundled sample result. Row 9 is a real server answer from 0876e03. "Demo" rows come from the scripted demo engine. No image shows a real home or dataset photos.
The autopilot on the synthetic replay at cbe0440, from onboarding to the AR view, at 3× replay speed:
house-scan-flow.mp4
Verification
swift test --package-path ios/HouseScanKit -Xswiftc -warnings-as-errors: 366 tests in 58 suites pass at d02cc11. They include:scene.schema.json;server/scene.py;NoUnseenClaimsTests, one case per overclaim path the review found.CI (Xcode 26.6) runs those tests, the XcodeGen drift check, the device build and every UI test with each screen's accessibility audit enforced. Green at d02cc11 (run 36279804539).
Local, Xcode 27, iPhone 17 Simulator (iOS 26.5), at e9e405d, before the field fix:
testFullFlowFromReplayandtestFullFlowFromLidarReplaypass.uv run python -m packet validatefrompacket/at d5439cf) accepts both exported packets as 1.1: 32 photos each, and 38 depth frames from the LiDAR replay.scene.jsonvalidates against the server schema. It carriessource: tap, amulchpatch andout_fton every wall entry.The hosted server (redeployed at 930e8e5) answers that scene
manual_review. Its three requests are correct for the fixture:out_ft6.500001 over −3.5 to 6.5 ft, for the gas, opening and wall-equipment checks, because the synthetic camera sees the wall only 6.5 ft up;With the mulch patch,
ground_surfaceis unsure only because the footprint sits within measurement error of the patch edge (margin); before, no surface was recorded at all.wall_backing,facing_gap,headroomand both route checks pass.Two fresh reviews at 35ac3f2: an Opus review of the whole diff and an Astra review of one question, whether any path exports coverage, a ground patch or a wall the camera didn't see. Fixed from them:
The Astra reviewer then re-checked at e9e405d. Its fixes held, and it found four more paths, fixed in b7f51ba:
The caretaker's Astra review of the field fix found two more, fixed in 5c8c739: a nearly parallel aim could put an end hundreds of metres away, and an object past an end before a corner named a wall the scene leaves out.
Not done, and what could go wrong
One real-iPhone run so far, and it lost the walk. Aiden ran TestFlight build 657ab28 at a mock wall (experiments: device field test runbook and run 1 of the TestFlight build #23,
experiments/device-field-test/README.mdon t3/device-field-test). "Can't get there" set each end at the unbroken covered stretch from the meter, which was 0 because the walk never asked for the ground in front of the meter. The scene therefore reported no wall and no ground. The fix (3c7b83a, merged at 75dea41) is described under Why it works this way; issues "Can't get there" during the walk can put the wall's end at the meter, and the rest of the walk is dropped #24, Tilt-down prompts never ask for the ground in front of the meter #28 and The "Is this the end of the wall?" prompt never appeared on a 19 ft walk #29 carry the details for Aiden's retest. His marks against a tape, from the meter's centre line: window right edge −0.5 in, sill +1.9 in, left edge +5.4 in, gas stand-in 4.8 in nearer the meter. All four are inside the server's allowance for AR taps (0.3 ft plus 0.16 ft per ft from the meter). These are the first device measurements.Most of the live path hasn't run on a device. Four things have only built and run in the Simulator: the live ARKit path (meter raycast, anchor refresh, ground refinement, relocalization), the plane
poseandboundary_mmath, depth-frame recording and the 7.5 ft capture height. Apple's documentation matches the plane convention the writer uses: since iOS 16 the anchor transform isn't rotated, and the extent'srotationOnYAxisis applied to the geometry. No device has confirmed its direction. A wrong sign would drop boundaries (logged), not break the packet. Every capture threshold is a guess, stated where it is defined.The 4.5 ft walking height rests on an assumed field of view. If a phone's view is narrower than the 30° to 31° half angle assumed, the walk will still ask for some tilt-ups. The camera intrinsics recorded in runs 2 and 3 can settle this.
The fixture never settles the 6.5 ft checks. The synthetic camera sees the wall 6.5 ft up at most, and its three tilt-up frames share one position. So every replay answer asks for one more wall view, and the autopilot says "I can't get there". A second tilt-up position in
Tools/make-synthetic-replay.swiftwould fix that.Coverage still stands in for visibility in three places:
S6's Map3D (ios: live 3D map as the coverage model where depth exists #21) replaces this coverage model. This PR stops adding occlusion to the old one.
One ground answer for the whole wall. A mixed wall can't be described. Answering "Driveway" or "Deck" fails the ground check at every spot, which the rules intend. Bare soil has no answer of its own, so a homeowner with a dirt bed will likely pick "Not sure".
Wall source and error. A wall on a detected plane gets 0.75 ft of default error on the server, while a tap on ARKit's estimated plane gets 0.3 ft. The estimated tap is likely the less accurate of the two. The phone reports the source truthfully and leaves the error values to the server's rules.
The UI tests never see a real server's request. They run on the sample result. The real-answer path is covered by package tests on a recorded server answer.
Timing flakes.
ScreenStatesUITests/testAddSomethingFromTheReviewMarksOnTheCamerafailed once locally, tapping a chip while the review fades back in, and then passed 3 of 3. The full-flow audit now waits 6 s before its second pass, after a system notification banner failed a contrast check on CI (run 36264789032).Main-thread copy at upload. Copying the mesh and planes for the packet runs on the main actor. Nobody has measured how long that stalls on a large mesh.
The ADVIO walk can't close its gap loop. Under the two-position rule it never covers the ground beside any assumed meter, so the autopilot skips the gap. That replay looks along a path, not at a wall.
Open findings on merged PRs, posted to Aiden as follow-ups:
WalkedEnd.sidecan fall back to the other side; the card loses the task's detail whilewallTooShortis set.wall_endmark.The 3D view frames the whole marked wall. On a long wall the battery looks small.
Checklist
🤖 Generated with Claude Code