Skip to content

fix(ios): combined app candidate: one-capture uploads, coverage rebuild cache, ground freshness, privacy manifest, AX5 folds, #56 and #49 guidance fixes - #191

Open
SamGu-NRX wants to merge 56 commits into
codex/public-coverage-20260930from
codex/public-assembly-20260930
Open

SamGu-NRX wants to merge 56 commits into
codex/public-coverage-20260930from
codex/public-assembly-20260930

Conversation

@SamGu-NRX

@SamGu-NRX SamGu-NRX commented Sep 30, 2026 •

Copy link
Copy Markdown
Owner

Head 75f6c37. Stacked on #188 (codex/public-coverage-20260930, bc21ace), which sits on #187 and #183. This PR is the combined app candidate. It merges #186, #190 and #188's fixes with their history and adds the changes below. #184 and #189 (corner shape, Sam's decision) and #192 (Hunter modules) are left out.

What changed

Merged in this update (75f6c37)

Each came through its own PR, reviewed and green on its head, and is merged here with its history:

  • ios: bind placement answers to the submitted scene #198, placement answers are bound to the scene that was sent (6ec6912). The client now checks the answer's stats.input_sha256 against the scene bytes it sent and refuses a missing, malformed or mismatched hash through the existing "We couldn't read the server's answer" recovery. The bundled sample is exempt.
  • fix(ios): results say they need an installer's review, never that one is coming #199, results say they need an installer's review (94f18ff). Nothing in the app contacts an installer, so the result, its footnotes, the AR title and the skip hints no longer promise that one will confirm, check or look. The headline for a result a person has to settle reads "Needs an installer's review".
  • ios: say why House Scan asks for local network access #201, a purpose string for the local network (032eea5). NSLocalNetworkUsageDescription explains that the app connects to a House Scan server on the local network only when a developer has set one up for testing.
  • CODEX-01, tap readiness in ScreenStatesUITests (b3c117d, test-only). The tap helper reads a button's frame and enabled state from one snapshot per poll against one deadline. Before, separate existence, layout and hittability waits used up the AR Done button's 20 s budget (37022177313 attempt 1). A control below the window now uses XCTest's own scroll-to-visible tap before any hit-point query; asking for a hit point first failed feature.ac in attempt 2. It adds TapReadiness with seven isolated tests and two regressions: an off-screen review chip at AX5, and closing AR through Done. Every identifier the helper taps resolved as a Button in the earlier hosted logs, so restricting the lookup to buttons drops no target.
  • fix(ios): where the space ends: ground contact, late and lost far walls, stable measurement (#168) #188's bc21ace, the laid-out-frame wait ported to the old tap helper, is merged so this branch contains its base again. Its one file keeps this branch's version: TapReadiness replaces that helper and rejects the same unsettled frames.

An upload sends one captured scene and packet (737d921)

On LiDAR phones the upload measures the mesh for what faces the wall and what is overhead, off the main actor. It measured against the wall as it was before that await, then built scene.json and the capture packet from the engine's live wall after it. A ground refinement or an anchor correction during the measurement put clearances measured against the old wall into a scene describing the new one (CodeRabbit's finding on bd6b933).

The upload now captures the scene (without its mesh fields), the wall in world meters, the mesh and the packet's inputs in one main-actor turn, and marks the scene packaged there (ScanEngine+Export.swift, ScanEngine.swift). UploadPackaging.package measures the captured mesh against the captured world wall and span. It checks that the upload is still current (same scan, not cancelled), then serializes the captured scene with those measurements and gives the same bytes to the captured packet (UploadPackaging.swift). saveBundle writes those inputs and rereads nothing.

  • A ground change during the measurement follows the packaged-upload rule below: the first change withdraws the upload and sends it again once, and a second one fails with "Try again". An anchor correction keeps the captured upload.
  • A withdrawn, cancelled or reset upload serializes nothing, writes no bundle and submits nothing.
  • After "It's clear", the bundle is written again from the upload's captured packet. Only the guidance log is reread, and it is placed with that packet's wall and meter frame. Before, the old scene was paired with a fresh mesh and the current wall.

The packet's recorder streams (trajectory, depth, motion) are still read when the packet is written. What holds is one wall and frame across the scene and the packet's geometry, not a byte-for-byte snapshot of one instant.

Coverage rebuilds reuse each kept frame's views (b926c92, a cherry-pick of the performance branch's 745a00ed)

When ARKit refines a plane that counts as a far surface (a corridor's far wall, a side yard's fence), the coverage map rebuilds from every kept frame on the main actor. Most of that work recomputes what each frame shows. The map now keeps those views per frame and reuses them while the frame's camera and depth, the wall chain, the ends and the far-surface lookup at every cell its depth readings consulted are unchanged (CoverageMap.swift). The merge still runs in capture order on every rebuild, so the recorded sightings, end clipping, past-limit replay and revision count are what they were. Any change to the wall or ends drops every kept view.

RebuildViewCacheTests runs a cached map and a full-rebuild map through the same changes and compares their whole stored state and public readers (RebuildViewCacheTests.swift).

On this Mac, across 17 synthetic seeded walks, total rebuild time fell 1.9× to 15.7× and 61 % to 97 % of views were reused, at about +0.25 to +1.9 MB of heap. Under the worst jitter a cached rebuild's p90 is still 5 to 23 ms, against 35 to 42 ms uncached, so one can still exceed a 16.7 ms frame. These are Mac timings with synthetic scenes and plane feeds. Phone speed and ARKit's real refinement rate are unmeasured, so no phone gain is claimed.

Answering a wall end no longer brings back a request that just stalled (#56)

When the walk's aim request (say, the ground by the meter) gains nothing for 20 s, the planner defers it and the walk moves on. Answering anything else then ("Wall ends here" and its question, "Can't get there", an overhead answer, a followed corner, a refused finish) called GuidancePlanner.reset(), which also cleared the deferred stretches. With the camera still near the meter, the same request came straight back with a fresh 20 s clock. Those actions now call settleCurrentTask(), which drops only the current task. reset() still clears everything, and is used at the start of the walk, for a new scan and for a spatial reset, whose new world frame makes old stretches meaningless (ScanEngine.swift, GuidancePlanner.swift).

A follow-up request stays available whenever its evidence is still missing (#49)

After the server's answer, the app suppresses a request that asks for the same view as one already asked, skipped or earlier in the same answer. It used a 0.1 ft allowance on the Float-meter span, while progress measures the server's exact span_ft and counts any uncovered edge of 0.01 ft or more as missing. A request for 2 to 8.09 ft after one for 2 to 8 ft therefore vanished, though its last 0.09 ft could never be met, and after a skip the result marked it not capturable. Same-view now requires the same band and kind of need, a reach no higher than the other's, and a span inside the other's, compared exactly in the requested feet progress uses. Rounding tolerance is applied only by progress, against real coverage, so it can't stack: a request for 2 to 8.0175 ft after 2 to 8.0085 ft, met by coverage to 8.0 ft, stays available. Equal and contained requests are still suppressed, and the request limit and "Show my result" are unchanged (GapPlanner.swift).

Picked up from other PRs

Earlier in this PR

A ground change can no longer leave a stale answer on screen

ARKit keeps refining the ground while the first upload is in flight. The scene sent to the server fixes the ground at packaging (ScanEngine+Export.swift:13-20). The spot check, 3D preview and AR draw against the current ground (ScanContract.swift:175-180). So a first answer could appear shifted by the change. GroundFreshness now treats a packaged first upload like an answer on screen: it withdraws the answer and resends once, and a second change before a new answer fails with "Try again". A change before packaging is ignored, because the scene is still built from the current ground.

A failed or refused upload has no answer up and none coming, so it maps to a new Screen.stopped, and a ground change keeps that screen. Before this, an offline failure after a ground resend lost its reason, and a refused resend lost "Back to review" and gained "Try again" for a scan the server had refused. The one-resend limit still applies when sending resumes.

At AX5 the see-behind card leads with what to do

Root's full-scale review of #190's own see-behind frame found the card covering the camera, down to "Can't see past it". When the card folds at accessibility sizes, "Look around it" now leads. Ride-along coaching and the reply stay in view. "Something is in front of the wall here, about 5 ft right of your meter. Look at it from the side or step around it." moves under Details. Instruction.folded carries those words, and default sizes are unchanged. ScanCopy.withCoaching now copies the task, so coaching keeps the folded words.

At AX5 the find-meter card folds and its button fits (B-43)

Root's manual pass at b6ee153 found the next step after "Allow camera" unusable at AX5. The card filled the camera with the meter's description, the reticle at the screen's middle sat under it, and "This is my meter" ran below the screen. The screen now folds like the walk's aiming steps:

  • "Find your electric meter" (or "Tap a spot on a wall" on a practice scan) stays on the card, and the description moves under Details.
  • At least 180 pt of camera stays open above the button.
  • The button shows its words without the pin, so "Put the sample meter here" fits.

The reticle and the button's mark point move to the middle of the open camera, the same mark a tap there makes (OpenCameraAim). "Hold on, finding your wall" leads with "Move your phone side to side". Coaching that replaces the task stays unfolded. Default sizes are unchanged.

Zero-width open camera, fixed in 2695268. At 0d4512b, OpenCameraAim required a non-zero width. The chrome's open camera is a Spacer that can be zero points wide, so at AX5 the reticle fell back to the screen's middle under the card (run 36877327250's frames; Greptile flagged it too). Only the height is required now, and OpenCameraAimTests feeds the zero-width window.

A privacy manifest for the required-reason APIs the app calls

The app had no PrivacyInfo.xcprivacy. This is a gap found while preparing a release, not an App Store rejection. The new file declares only what the code calls:

Category Reason Use
UserDefaults CA92.1 The Practice meter switch, the app's own key
File timestamp C617.1 ScanFolderCleanup reads scan.zip dates in the app's Caches
System boot time 35F9.1 Time between "can't get there" taps; the recorder's restart cutoff
System boot time 8FFB.1 The wall-clock time of the first recorded frame, the packet's capture.started_at

It makes no tracking or collected-data claims; those are App Store privacy answers. PrivacyManifestTests fails when shipped Swift uses a category the manifest lacks, or the manifest declares one nothing uses, and pins each reason.

Smaller changes

Verification

At 75f6c37:

At 0b35f65 (test-only on top of b926c92; app, package and project sources are byte-identical):

  • Full hosted run 37022244049 (workflow_dispatch) passed 644 package tests in 91 suites and 47 UI tests with 1 skip and 0 failures. Those UI tests include the every-state audit, both changed tests and testWholeFlowThroughTheButtons.
  • Pull-request run 37022177313 failed one UI case on each of its two attempts, a different case each time. Both cases passed in 37022244049 and in both b926c92 runs.
    • Attempt 1 ran 46 UI tests with 1 skip and 1 failure. testWholeFlowThroughTheButtons reported "missing or not tappable: action.closeAR" on the AR screen. One existence query for the button took 7 s and later queries 3 to 4 s each, so the tap helper's 20 s budget ran out; in the passing runs the button was found in about 1 s.
    • Attempt 2 was the one rerun of the failed job, at the same head. It also ran 46 UI tests with 1 skip and 1 failure. testWholeFlowThroughTheButtons passed this time, and testAddSomethingFromTheReviewMarksOnTheCamera failed instead: XCTest reported "Activation point invalid" for feature.ac on the review, about 4 s after the review appeared, while the helper waited for it to become hittable.
    • The app code is byte-identical to b926c92. The timings point to slow accessibility answers on the hosted runner, but the cause is unproven and no reliability fix is claimed. The pull-request run is still red on this head. It isn't a branch-protection requirement (the repository has no rulesets), but a green run on the current head is the candidate's acceptance target.
  • Both hosted runs at b926c92 failed one case, testRejectedUploadGoesBackToReview: 37013846307 ran 28 UI tests with 1 failure, and 37013940503 ran 46 with 1 skip and 1 failure, its every-state audit passing. The test runs on the demo engine, which this update doesn't touch. It tapped "I can't get there" on the review's own gap request while the demo's script was covering that request and sending by itself, about 3.9 s after it opened. The tap's event came 3.2 to 3.5 s into the wait in the passing bd6b933 runs, and after 4.3 s in both b926c92 runs, when the button had gone. This was a race in the test, not an app regression.
  • 0b35f65 lets the script finish and goes on to the spot check, with the test's other assertions and waits unchanged (ScreenStatesUITests.swift). No other test checked that skipping sends the scan, so the new testSkippingTheGapRequestSends taps "I can't get there" on a frozen gap request and expects the upload screen. Locally both tests passed on the iPhone 17 Simulator.
  • Root's manual pass on the b926c92 build in the Simulator still applies, since the app code is unchanged:
    • The dark walk and gap cards show their advice, and their "Can't get there" actions advance.
    • The AR view opens from the result, is labeled as an example spot, and returns.
    • A real capture sent to an unreachable server shows "Try again" and "Share scan". A manual retry fails the same way, and Share scan offers a 2.1 MB ZIP.
    • The rejected fixture's "Back to review" keeps the review's answers.
    • That pass didn't cover VoiceOver, Reduce Motion, or anything on a phone.

At b926c92:

  • Local runs on the same tree (built as d8192b15, which differs only in its commit message), under the shared Mac's queue:
    • 644 package tests in 91 suites passed. This is the first full run that includes the cache's last test, viewsFollowTheLookupWhereReadingsMetTheFence.
    • On the iPhone 17 Simulator, 12 UI tests ran with 1 skipped and 0 failures: GroundFreshnessUITests (2), FullFlowUITests including the LiDAR replay (6, with the real-recording skip), SourceRestartUITests (1), and the share, rejected-upload and offline screens (3). testPracticeMeterFromReplay was left out locally because that Simulator's Vision can't create an inference context; it runs in the hosted suites.
  • UploadPackagingTests (7 tests) runs the real package and the real mesh measurement behind a gate that holds the measurement while the test changes the source.
    • With nothing changed, the facing and overhead measurements come from the world's ground, set 0.5 m up.
    • When ground, wall, span and marks change mid-measurement, the scene bytes and packet geometry stay the captured set.
    • An anchor correction keeps the capture without a resend. A ground change rejects the old completion and sends the replacement, and a second change keeps the failure.
    • A reset or a cancel sends nothing, and a scan without a mesh or a packet still sends its scene.
    • Removing the post-measurement check, or measuring against the ground-zeroed scene wall, fails 6 of the 7 tests.
  • Limit of those tests: the app has no unit-test target, so a small stand-in follows upload()'s order, with the real GroundFreshness deciding. The engine's own wiring is covered by review and the Simulator runs. Replay UI tests have no mesh, so no UI test reaches this await.
  • The cache's tests catch both injected faults in what it records as read. Its comparison probe checked final state only; the per-change comparison is the test suite. Separate Sol and Astra reviews of 745a00ed found nothing.

At bd6b933: full run 37001843593 passed 631 package tests and 46 UI tests, every-state audit included, with 1 skip and 0 failures. Push run 37001782118 passed 631 package and 45 UI tests with 1 skip.

Earlier heads, kept for history: full run 36991598066 at 693271e passed 625 package tests and 46 UI tests (1 skip, 0 failures) with the every-state audit. Full run 36860048623 at b6ee153 passed the audit. Focused runs at 0d4512b, d88f762, efb7ab6 and 1f7e076 each failed one UI case that a later commit fixed. A local run at d837c4e stalled 36 s after the spot check in testCantGetThereEndsWhereThePhoneIs, with the same engine decisions as hosted; the cause is unproven, and the test passed at bd6b933.

UI changes

The dark coaching line is longer. Before is wallWalk-tooDark in run 36991598066 (693271e); after is the same state in run 37001843593 (bd6b933). Matched captures are pending root's visual check. The upload and cache changes show nothing new on screen.

Earlier in this PR, at AX5 on the iPhone 17 Simulator: the folded see-behind card and its Details (wallWalk-seeBehind-AX5-* in run 36860048623), and the folded find-meter card (findMeter-AX5-*).

Risk

  • After an upload, the spot check keeps that upload's packet, mesh included, in memory until the scan resets.
  • Nothing in this update ran on a phone: the upload repair, the cache's speed (measured only on a Mac), relocalization clearing the dots, the planner on a real walk, and server requests from the deployed backend after the same-view change are unverified.
  • Exact containment can raise a request again when the server works it out with an edge a hair wider after "I can't get there". The automatic request limit and "Show my result" still end the loop.
  • Nothing ran on a phone. VoiceOver, Reduce Motion rendering, AX1 to AX4 and other screen sizes weren't checked. The physical tilt from arrow to ring isn't exercised by the frozen demo.
  • No UI test can make an upload fail, so the stopped-screen mapping in ScanEngine is covered by the pure rule test and review only.
  • The shared packet carries capture.started_at (8FFB.1) beside uptime-based ARKit frame times. A receiver could subtract them to estimate boot time. Apple's wording doesn't settle whether that pair counts as sending information derived from boot time. The packet contract is unchanged and the question is open.

Checklist

  • The scope is focused; related changes belong together
  • I explained what changed and why
  • N/A before/after images in the body: the dark-copy frames are named above and await root's visual check.
  • N/A video: no motion changes in this update.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes
    • Ground refinements during upload now account for whether the scan has been packaged. Packaged scans are resent without showing the withdrawn answer; un-packaged uploads continue, while further refinements during a pending resend fail rather than risk showing an outdated result.
    • Upload results and saved scans now use the same captured scan data, including after a clear spot check. Answers are checked against the submitted scan before they’re displayed. Skipping guidance preserves deferred tasks, and aiming no longer accumulates walking-speed warnings.
  • Accessibility
    • At larger text sizes, camera instructions and legends can fold into Details, wall-walk actions stack vertically, and result badges move above the answer card. Camera guidance and find-meter controls adapt to the visible camera space, and onboarding pages remain accessible.

SamGu-NRX and others added 5 commits September 30, 2026 03:19
The reader's catch around VNImageRequestHandler.perform returned .noNumber
without a trace. On one Mac's iOS 26.5 simulator Vision throws
com.apple.Vision code 9, "Could not create inference context", so Practice
replay failed there with nothing in the app log to say why.

Log the error on RuntimeLog.engine: domain and code public, Vision's
message private, since it is framework free text nobody has vetted. The
returned readout is unchanged.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…embly branch

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
At the largest text sizes:
- Wall-walk action pairs stack full-width instead of breaking their
  labels into one or two letters a line (B-28).
- The instruction card ends above the middle of the screen and its words
  scroll inside it, with the reply pinned. The aim ring shows only where
  it is wholly in the open camera between the card and the actions,
  measured from the layout rather than fixed bands (B-36).
- Onboarding pages are clipped above an opaque footer, so text no longer
  runs under the page dots. The illustration's "About 2 min" stops
  growing past xxxLarge; the page text states the duration.
- The result's Replay and sample badges sit above the answer card
  instead of over the 3D view.

UI tests check stacking and button height, the ring against the card,
reply and actions, the dots row's pixels, and badge placement.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…nswer and resends once

The scene fixes the ground when it is packaged, but ARKit keeps refining the ground while the
upload is in flight, and the spot check, 3D preview and AR draw the answer against the current
ground. A first upload's answer could therefore show a spot shifted by the ground change.
GroundFreshness now treats a packaged first upload like an answer on screen: withdraw and resend
once, and fail if the ground changes again before a new answer is shown. A change before
packaging still keeps the upload, because the scene is built from the ground as it is then.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ilure instead of resending

dcaaf7b left the packaged flag set after the request failed or was refused, so a later ground
refinement withdrew an answer that was never coming and sent the scan again without a tap,
skipping the refusal's review. The upload state now decides: a failed or refused upload has no
pending answer, which restores #187's behavior on those screens. Found by a fresh Sol review.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Sep 30, 2026 •

Copy link
Copy Markdown

Review in Change Stack →

Navigate logical layers of code changes, visualize relationships, and explore their blast radius.

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
🧰 Additional context used
📚 Code guidelines (1)
ios/README.md — configured

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration
  • Configuration used: Organization UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: cd0c6075-e6ee-4e97-a625-0fca232ce6f9
📥 Commits

Reviewing files that changed from the base of the PR and between 0b35f65 and 75f6c37.

📒 Files selected for processing (15)
  • ios/Config/Info.plist
  • ios/HouseScan/Runtime/ResultClient.swift
  • ios/HouseScan/UI/Copy/ScanCopy.swift
  • ios/HouseScan/UI/Copy/SpotCheckCopy.swift
  • ios/HouseScan/UI/Preview/DemoEngine.swift
  • ios/HouseScan/UI/Screens/GapRequestScreen.swift
  • ios/HouseScan/UI/Screens/MeterCloseUpScreen.swift
  • ios/HouseScan/UI/Screens/ResultARScreen.swift
  • ios/HouseScan/UI/Screens/ResultScreen.swift
  • ios/HouseScanKit/Sources/HouseScanKit/Scene/ResultBinding.swift
  • ios/HouseScanKit/Tests/HouseScanKitTests/ResultBindingTests.swift
  • ios/HouseScanUITests/FullFlowUITests.swift
  • ios/HouseScanUITests/InstallerReviewUITests.swift
  • ios/HouseScanUITests/ScreenStatesUITests.swift
  • ios/HouseScanUITests/TapReadiness.swift
🚧 Files skipped from review as they are similar to previous changes (2)
  • ios/HouseScan/UI/Screens/ResultScreen.swift
  • ios/HouseScan/UI/Copy/ScanCopy.swift

Included review availability: This review used your included allowance. Your plan provides up to 1 included review per hour; 0 remain after this review.


📝 Walkthrough

Walkthrough

The changes update upload packaging and freshness decisions, validate server answers against submitted scenes, revise guidance rules, and adapt camera and screen layouts for accessibility text sizes. They also add coverage-map rebuild caching, privacy declarations, and Vision failure logging.

Changes

Upload freshness and guidance

Layer / File(s) Summary
Ground-change resend policy
ios/HouseScanKit/Sources/HouseScanKit/Upload/GroundFreshness.swift, ios/HouseScan/Runtime/ScanEngine.swift, ios/HouseScanKit/Tests/HouseScanKitTests/GroundFreshnessTests.swift, ios/HouseScanUITests/GroundFreshnessUITests.swift
GroundFreshness distinguishes stopped uploads and uses packaging state to decide whether a ground change keeps an upload, requests a resend, or fails. Tests cover the decisions and resend flow.
Captured upload packaging
ios/HouseScan/Runtime/ScanEngine+Export.swift, ios/HouseScan/Runtime/ScanEngine+Packet.swift, ios/HouseScan/Runtime/ScanEngine+Confirm.swift, ios/HouseScan/Runtime/ScanEngine.swift, ios/HouseScanKit/Sources/HouseScanKit/Scene/UploadPackaging.swift, ios/HouseScanKit/Tests/HouseScanKitTests/UploadPackagingTests.swift, ios/HouseScan/Runtime/LaunchOptions.swift, .github/workflows/ios.yml
ScanEngine captures upload inputs and passes them to packaging. UploadPackaging measures and serializes captured inputs, checks whether the upload is current, and attaches the scene bytes to the packet. Bundle saving and exchange logging use the resulting packet.
Guidance task settlement and coaching
ios/HouseScanKit/Sources/HouseScanKit/Guidance/GuidancePlanner.swift, ios/HouseScanKit/Sources/HouseScanKit/Guidance/CoachingDebouncer.swift, ios/HouseScan/Runtime/ScanEngine.swift, ios/HouseScanKit/Tests/HouseScanKitTests/GuidancePlannerTests.swift, ios/HouseScanKit/Tests/HouseScanKitTests/CoachingDebouncerTests.swift
GuidancePlanner settles the current task without clearing deferred stretches. CoachingDebouncer clears moving-fast evidence while aiming. Tests cover both behaviors.
Gap progress and same-view comparisons
ios/HouseScanKit/Sources/HouseScanKit/Guidance/GapPlanner.swift, ios/HouseScanKit/Tests/HouseScanKitTests/ServerGapTests.swift
GapPlanner measures progress and requested spans in feet. It compares requested spans and reaches without the previous tolerance. Tests cover contained spans, extensions, changed reaches, and coverage rounding.

Scene answer validation

Layer / File(s) Summary
Bind server answers to submitted scenes
ios/HouseScanKit/Sources/HouseScanKit/Scene/ResultBinding.swift, ios/HouseScan/Runtime/ResultClient.swift, ios/HouseScanKit/Tests/HouseScanKitTests/ResultBindingTests.swift
ResultBinding checks that a server answer contains a valid SHA-256 matching the submitted scene. ResultClient checks successful responses before returning them. Tests cover matching hashes and refusals.

Accessibility layouts

Layer / File(s) Summary
Folded instruction and camera guidance
ios/HouseScan/UI/Components/CameraChrome.swift, ios/HouseScan/UI/Components/InstructionCard.swift, ios/HouseScan/UI/Copy/ScanCopy.swift, ios/HouseScan/UI/Camera/*, ios/HouseScan/UI/Screens/GapRequestScreen.swift, ios/HouseScan/UI/Screens/WallWalkScreen.swift, ios/HouseScanUITests/ScreenStatesUITests.swift
At accessibility text sizes, instruction details and the legend can appear under Details. Camera chrome reports the open camera region, which screens share with the wayfinding overlay.
Find-meter aim positioning
ios/HouseScan/UI/Screens/FindMeterScreen.swift, ios/HouseScanKit/Sources/HouseScanKit/Overlay/OpenCameraAim.swift, ios/HouseScanKit/Tests/HouseScanKitTests/OpenCameraAimTests.swift, ios/HouseScanUITests/ScreenStatesUITests.swift
FindMeterScreen uses the measured camera region to position its reticle and marking action at accessibility text sizes. OpenCameraAim calculates the visible region midpoint. Tests cover positioning.
Onboarding, result, and accessibility UI validation
ios/HouseScan/UI/Screens/OnboardingScreen.swift, ios/HouseScan/UI/Screens/ResultScreen.swift, ios/HouseScanUITests/ScreenStatesUITests.swift
Onboarding content is clipped and its footer receives an opaque background. Result badges move above the answer card at accessibility text sizes. UI tests cover these layouts and related interactions.
Installer wording
ios/HouseScan/UI/Copy/ScanCopy.swift, ios/HouseScan/UI/Copy/SpotCheckCopy.swift, ios/HouseScan/UI/Screens/GapRequestScreen.swift, ios/HouseScan/UI/Screens/MeterCloseUpScreen.swift, ios/HouseScan/UI/Screens/ResultARScreen.swift, ios/HouseScan/UI/Preview/DemoEngine.swift, ios/HouseScanUITests/FullFlowUITests.swift, ios/HouseScanUITests/InstallerReviewUITests.swift
Installer-related result copy and accessibility text is revised. UI tests check sample and demo result wording.

Coverage rebuild-view cache

Layer / File(s) Summary
Visibility views and cache reuse
ios/HouseScanKit/Sources/HouseScanKit/Coverage/CoverageMap.swift, ios/HouseScanKit/Tests/HouseScanKitTests/RebuildViewCacheTests.swift
CoverageMap separates visibility calculations from state recording and reuses per-frame views when frame inputs, lookup reads, and cache basis match. Tests compare cached rebuilds with full rebuilds.

Privacy and platform updates

Layer / File(s) Summary
Privacy declarations and validation
ios/HouseScan/PrivacyInfo.xcprivacy, ios/HouseScanKit/Tests/HouseScanKitTests/PrivacyManifestTests.swift
The manifest declares User Defaults, file timestamp, and system boot time access reasons. Tests scan shipped Swift source and verify manifest categories and reason codes.
Local network prompt and build timeout
ios/Config/Info.plist, .github/workflows/ios.yml
The app adds a local-network usage description for developer-configured testing. The iOS build timeout increases to 75 minutes.

Diagnostics

Layer / File(s) Summary
Vision request failure logging
ios/HouseScan/Runtime/MeterReader/VisionMeterNumberReader.swift
The Vision request failure path logs the error domain and code as public fields, and the localized description as a private field. It still returns retakeNoNumber.

Priority: ➖ Normal

Estimated code review effort: 4 (Complex) | ~60 minutes

Change: Bug fix

Sequence Diagram(s)

sequenceDiagram
  participant ScanEngine
  participant GroundFreshness
  participant UploadPackaging
  participant ResultClient
  participant Server
  ScanEngine->>GroundFreshness: Evaluate ground change and packaging state
  GroundFreshness->>ScanEngine: Return keep, resend, or fail action
  ScanEngine->>UploadPackaging: Package captured scene, mesh, and packet
  UploadPackaging->>ScanEngine: Return packaged scene and packet if current
  ScanEngine->>Server: Submit packaged scene
  Server->>ResultClient: Return answer
  ResultClient->>ResultClient: Check answer hash against submitted scene
Loading

Suggested reviewers: aidenjohnston

Merge Risk: ⚪ Minimal · up to 75f6c

The previously identified risk of uploading measurements from the wrong wall is resolved. No actionable merge-blocking issue remains after normal checks.

Security Architecture Review

Security architecture risk: 🔵 Low · up to 75f6c

The reviewed upload path strengthens answer validation and keeps submitted scan data consistent with its captured geometry. No introduced security weakness was established. Some persistence and broader security coverage remains incomplete, so the assessment is not a complete assurance of safety.

Retained concerns
No architecture-level concerns identified.

Security review details

Security Blast Radius

  • observed — The inspected network exposure remains the device's scene JSON sent to its configured placement server. The request authority, endpoint and body are unchanged from base; the local capture packet is written separately and is not uploaded by this path.

Trust Boundaries and Controls

  • inferred — The new hash check rejects answers declaring another input, but does not authenticate the server or prove the answer's substantive correctness. A malicious configured server can compute the submitted scene's hash and return false content with a matching hash; that server trust relationship predates this PR.
  • observed — The bundled-sample exemption is isolated to the offline SampleResultClient. Selecting HTTPResultClient always applies response binding; the sample branch does not bypass validation for a network response.

Resilience and Maintainability Implications

  • observed — Reset invalidates answer and shareable-scan publication, but a detached packet write already in progress can finish before its post-write guard rejects publication. The inspected reset and writer do not explicitly erase that completed archive. This persistence pattern predates the PR; it is not an established introduced security concern.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 61.88% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 223 functions across 43 files. (1 skipped… Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title identifies the combined iOS changes, including upload consistency, coverage caching, ground freshness, privacy, accessibility, and guidance fixes. It is long, but it clearly describes the ch…
Description check ✅ Passed The description covers the required What changed, Why, UI changes, Verification, Risk, and Checklist sections. It includes detailed behavior, test results, known limitations, and applicable media note…
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Full details: Docstring Coverage

Explanation

Docstring coverage is 61.88% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 223 functions across 43 files. (1 skipped: 1 unsupported.)

  • Fix all pre-merge checks with AI
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Commit to this branch
  • Create a new PR
  • Autopilot · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Autopilot is currently an internal CodeRabbit preview.


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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 30, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-03T09:07:41.088256Z 75f6c37 Manual request
ℹ️ 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" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@github-actions github-actions Bot added the size:M 30-99 effective changed lines. label Sep 30, 2026
@SamGu-NRX SamGu-NRX added the full-ui iOS CI also runs the every-state accessibility audit (about 10 more minutes). label Sep 30, 2026
@codemetrics-ai

Copy link
Copy Markdown

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 BaseScanning to your team's repositories.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 1f7e076b17

ℹ️ 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".

Comment thread ios/HouseScanKit/Sources/HouseScanKit/Upload/GroundFreshness.swift
SamGu-NRX and others added 3 commits September 30, 2026 10:52
The AX5 aim state never drew a frame on CI run 36733592843: the card's
height came from measured state (words, reply and status-row heights)
fed back into its own layout. The status row and card now go through a
small Layout that offers the card the height left above the screen's
middle, and the card's words use ViewThatFits to scroll only when they
don't fit. Nothing measured feeds back into the card.

- The open camera's frame lives in an observable object that only the
  aim overlay reads, so scrolling no longer re-evaluates the screen.
- The ring's fit includes its horizontal footprint and pulse; the band is
  cut to the screen, and a short band puts the arrow on its middle line.
- A target drawn under the card or actions gets an arrow pointing to
  where it is drawn, instead of the optical-axis direction, which can be
  undefined at the centre.
- Default-size action rows are the original HStack again; the Practice
  replay audit flagged "Done with this wall" as clipped with the
  AnyLayout row.
- Reduce Motion drops the legend-driven resize animation.
- Tests scroll the capped words until the legend shows with the reply
  fixed, and scroll each onboarding page to its end before sampling.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
At AX5 the two-line pill's capsule ends cut into "not from the server"
(hosted screenshot at 50e1069).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- A target at the middle of an open camera too short for its ring no
  longer shows a ring over the card.
- The aim test scrolls the card's words until the legend's end shows,
  checks the content moved and the reply stayed; the AX5 legend is
  taller than the words' viewport, so full containment was impossible.
- The onboarding test scrolls each page until its last element sits
  above the footer, instead of trusting a heading that stopped moving.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@SamGu-NRX SamGu-NRX removed the full-ui iOS CI also runs the every-state accessibility audit (about 10 more minutes). label Sep 30, 2026
@SamGu-NRX SamGu-NRX closed this Sep 30, 2026
@SamGu-NRX SamGu-NRX reopened this Sep 30, 2026
SamGu-NRX and others added 9 commits September 30, 2026 12:27
…end check

The every-state audit at 33c43fd (run 36743027718) failed contrast with
no element on every AX5 camera state whose words scroll, the only place
the always-visible inner scroll bar appears. It now shows while
scrolling and flashes once on appearance.

At AX5 one onboarding move is taller than the page's viewport, so the
page's end is when the last element's bottom shows above the footer.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Scrolling the card's words inside the card failed the every-state
audit's contrast check on every AX5 camera state: the audit samples each
text's full frame, and text clipped by the card's own scroll area lands
on the reply or the camera (run 36751580657). The card is back to its
e703199 layout at every size.

What stays for B-36: at accessibility sizes the chrome keeps a 180 pt
open camera between the card and the actions, which scrolls into view,
and the aim ring and edge arrow show only inside it, so neither is drawn
under the card, the legend or the actions. With less of the window on
screen than an arrow's disc, neither shows.

The legend again shows whenever a filling ring's target is on screen, as
at e703199, so it no longer depends on the measured window and the
per-step latch is gone. The AX5 aim audit state expects the legend; the
ring's progress is checked where it appears while the chrome scrolls.
The onboarding test swipes the page's own scroll view, since a drag at a
screen point left page two unmoved.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…e target

- Near the card or the actions, a ring that doesn't fit at its size shows
  at the largest radius that fits whole, down to the smallest ring's.
- A target inside the arrow lane put the lane's edge past it, with the
  arrow pointing back; the arrow now stops a disc short of the target.
- Each onboarding page's scroll view has an identifier, and the test
  swipes the one on screen rather than the first scroll view it finds.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
At bec7935 the every-state audit failed contrast with no element in the
four AX5 walking states whose open camera starts on screen (run
36761500732). Their screenshots show one new thing: an edge arrow in the
open camera pointing at the target under the card, over the path's
dots. At e703199 those states drew the ring out of sight under the
card and passed. A target on screen but under the card or the actions
now draws nothing until the open camera reaches it; arrows stay for
targets off screen or at its sides.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
… get

The every-state audit fails an issue only when two passes find it, and
scrolls between them to bring a cut-off element into full view. An issue
reported with no element has no frame, so nothing scrolled, and the four
AX5 walking states whose open camera reaches the bottom edge failed
contrast with no element (run 36768462318). With such a finding, the
screen now scrolls one step before the second pass. An issue still on
screen after it fails the test as before.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
With a 180 pt open camera kept below the card at accessibility sizes,
the every-state audit failed contrast with no element on the four AX5
walking states whose short card leaves that camera on screen, in three
runs (36761500732, 36768462318, 36775276861), and a scroll step between
passes did not clear it. The node it measured is not identified. The
spacer is back to zero at every size, as at e703199, whose AX5 states
passed; the frameless-finding scroll is reverted.

The ring still shows only where it fits in the measured open camera, so
it is never drawn under the card; at AX5, where the chrome overflows,
that means it is not drawn. The B-36 test now checks that invariant
across the chrome's scroll positions.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
At the accessibility sizes, on a step where the homeowner aims the
camera, the instruction card keeps the task, the eyebrow, ride-along
coaching such as "Slow down" and its reply in view, and folds the detail
and the aim ring's legend under a "Details" DisclosureGroup, the word and
control the result screen uses. The chrome keeps 180 pt of open camera
under the card on those steps; the actions scroll into reach. A target
under the card or actions gets an arrow in the open camera pointing to it.
Questions, marking, refusals and coaching that replaces the task keep
every word in view. Default sizes render as before.

Ride-along coaching now travels in Instruction.note instead of being
appended to the detail, so folding the detail cannot hide it. Stacked
wall-walk actions drop their icons, which broke "Mark something" into
three lines at AX5.

The rejected-upload test uses the pass sample and skips the review's own
view, so no single wait covers a second capture, two uploads and the
server's follow-up (run 36736877861 timed out still uploading).

A temporary audit of the seven aiming states at AX5 stands in for the
every-state run while this iterates.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Unhandled, XCTest attaches its own picture of each contrast issue, which
the shared handler suppresses; this names the node behind the finding
without an element. Removed before the final run.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…tapping

At AX5 the aim step's spot sits under the folded card, so an arrow in
the open camera stands in for the filling ring (run 36820279307). Hidden
like other arrows, it left VoiceOver without the spot's progress; an
arrow that stands in for a filling ring now reads "Spot to show, above"
and the ring's percent. Other arrows stay hidden.

The AX5 aim test now checks the ring or that arrow sits in the open
camera and reads 50 percent, instead of expecting a ring that the frozen
demo can't move into view.

The shared tap helper waits for a finite frame before asking whether an
element is hittable: result.details existed mid-transition with an
infinite frame and XCTest failed on its activation point.

The temporary aiming-state audits passed and are replaced by one on the
AX5 aim state and the off-screen aim state, whose arrow is newly read.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The fold now hides only a second line that explains. It stays off where
that line is what to do now: looking past an obstruction, a step back
added to an aim step, marking the next wall, and gap requests for ground
further out or a walk out, whose live "this needs" reading or "tap I
can't get there" are the instruction. Found by the fresh audit.

The aim arrow that reads progress is marked as updating frequently. The
tap helper checks every frame component and fails loudly when an element
is never laid out, instead of asking whether it is hittable anyway.

The temporary audit test is removed; the every-state audit covers its
states.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…s checked frozen (#191)

testRejectedUploadGoesBackToReview tapped "I can't get there" on the review's
own gap request while the demo's gap script was covering that request and
sending by itself, about 3.9 s after the request opened. The tap's event came
3.2 to 3.5 s into the wait when the test passed (runs 37001843593,
37001782118). On runs 37013846307 and 37013940503 at b926c92 it came after
about 4.3 s, the button had gone, and the tap failed. The test now lets the
script finish and goes on to the spot check, keeping its refused-upload, no
"Try again", back-to-review, unanswered-ground, resend, spot-check and result
assertions with the same waits.

testSkippingTheGapRequestSends checks the skip on its own: frozen
(-uiDemoFreeze), the gap script never runs, and "I can't get there" must open
the upload screen. No other UI test tapped it; ScreenStatesUITests' controls
table only checks that the button is offered.

Test-only; no app code changes.

Co-Authored-By: Claude Code <noreply@anthropic.com>
@SamGu-NRX

Copy link
Copy Markdown
Owner Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Oct 2, 2026 •

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

…s sent

The placement client decoded the answer's stats.input_sha256 but never
compared it with what it sent (#144). The server hashes the exact scene
bytes it parses: for a bare scene.json that is the request body (server/api.py
_parse, server/scene.py). The result schema requires the field as 64
lowercase hex digits.

HTTPResultClient now runs ResultBinding.check on every 2xx answer before
returning it. The check reads stats.input_sha256 with JSONDecoder, requires
the schema's form, and compares it with the SHA-256 of the bytes sent. It
fails closed:
- an answer it can't read, or one without the field, is refused as
  noInputHash;
- a value that isn't 64 lowercase hex digits is refused as
  malformedInputHash;
- a hash for another scene is refused as mismatch.

Each refusal reaches the existing "We couldn't read the server's answer"
recovery and isn't retried. SampleResultClient answers no particular scene
(its hash is all zeros), so it stays explicitly unchecked. The general
PlacementResult decoder and the test fixtures are unchanged.

This is defence in depth. Nothing shows the current server sending an answer
for another scene.

ResultBindingTests is built from a real server answer to the synthetic
replay. It covers a matching answer, a mismatch, an absent, null or numeric
hash, an uppercase, short or non-hex hash, a body that isn't JSON, and a
mismatched answer carrying an unparseable number elsewhere.

Co-Authored-By: Claude Code <noreply@anthropic.com>
…oming

The app shows the server's answer and contacts nobody, yet the result and the
scan's skip hints promised an installer's action: "An installer will confirm",
"An installer will check this", "An installer confirms this on site", "an
installer reviews every result for now", "The spot an installer will check" and
"An installer will look at it instead". Nothing sends a scan to an installer or
queues a review.

The result now says what it needs:
- The headline for a result a person has to settle reads "Needs an installer's
  review"; the other three answers are unchanged.
- An unsure check that no photo can settle, and a "Still needed" view the phone
  can't capture, read "Needs an installer to check" (one constant).
- The footnotes read "An installer has to confirm the spot on site before a
  battery goes in." and "The placement rules aren't final yet, so every result
  needs an installer's review for now."
- The AR view says "The spot an installer needs to check".
- Skip hints on the walk, the gap request, the close-up and the spot check say
  an installer "would need to" look, read or check instead.
- The offline sample's summary says the spot "needs an installer's review".

FullFlowUITests.testFullFlowFromReplay, which ends on the bundled sample (a
manual review under unapproved rules), now asserts the rendered headline and
both installer footnotes word for word. It skips them against a real server,
whose answer can be any decision. The server's own summary and reasons aren't
covered.

Co-Authored-By: Claude Code <noreply@anthropic.com>
SamGu-NRX and others added 2 commits October 2, 2026 14:07
The developer flow in ios/README.md points the app at a House Scan server
on a laptop on the same Wi-Fi. Info.plist already allows plain http to
local-network addresses (NSAllowsLocalNetworking), but it had no
NSLocalNetworkUsageDescription. iOS asks for local network access in that
flow, so the prompt had no purpose text (#144, second half).

The new string says the app connects to a House Scan server on the local
network only when a developer has set one up for testing. It claims no
Wi-Fi scanning. Homeowners use the hosted https server and never reach
this.

Checked with plutil -lint and a PlistBuddy read-back. Info.plist is
hand-maintained (INFOPLIST_FILE in project.yml), so the project file is
unchanged. The prompt on a physical phone is not yet checked.

Co-Authored-By: Claude Code <noreply@anthropic.com>
…n Details (#199)

"An installer has to confirm the spot on site before a battery goes in." shows
on every result, including "Not on this wall" and "We couldn't measure your
wall", which have no spot (Codex review on #199). It now reads "Before any
battery goes in, an installer has to confirm where it goes on site."

InstallerReviewUITests holds the demo's review sample still, opens Details and
checks that the window check, which needs a person, reads "Needs an installer
to check" while the ground check, which needs a photo, keeps "One more photo
would settle this" (Greptile review on #199). XCUITest can't read accessibility
hints, so the skip hints remain checked by review only.

Co-Authored-By: Claude Code <noreply@anthropic.com>
SamGu-NRX and others added 7 commits October 2, 2026 14:21
ScanEngine logs an upload failure's description with public privacy.
ResultBinding.Refusal's description included the full SHA-256 of the scene
sent, and for a malformed value the server's declared text (Greptile on
#198). A scene's hash fingerprints the homeowner's scan, so it shouldn't be
in the device log.

The description now says what went wrong and, for a malformed value, its
length. The hashes stay in the enum cases for callers and tests.
aRefusalsDescriptionNamesNoHash checks all three refusal kinds.

Co-Authored-By: Claude Code <noreply@anthropic.com>
The demo's review sample still offers a view to capture, so its headline is "One
more look", not the installer answer (hosted run 37052663637). The installer
headline stays checked on the bundled sample in testFullFlowFromReplay, which
passed in that run. This test keeps the footnote and the two unsure notes.

Co-Authored-By: Claude Code <noreply@anthropic.com>
Read layout and enabled state from one snapshot per poll. Let real taps
scroll bottom-edge controls before hit-point queries, and stop issuing
reads when the original readiness deadline is exhausted.

Co-Authored-By: Claude Code <noreply@anthropic.com>
…branch: bind placement answers to the scene that was sent

Co-Authored-By: Claude Code <noreply@anthropic.com>
… branch: results say they need an installer's review

Co-Authored-By: Claude Code <noreply@anthropic.com>
… branch: declare why the app may use the local network

Co-Authored-By: Claude Code <noreply@anthropic.com>
) into the assembly branch

Co-Authored-By: Claude Code <noreply@anthropic.com>
@SamGu-NRX

Copy link
Copy Markdown
Owner Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Oct 2, 2026 •

Copy link
Copy Markdown
⚠️ Action not completed

Review rate limited.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

…bly branch

bc21ace ports #190's laid-out-frame wait into the old tap helper. This branch already had that wait, and CODEX-01's TapReadiness (b3c117d) replaces the helper; it rejects null, infinite and invalid frames before any hit-point query. The conflict in ScreenStatesUITests.swift keeps this branch's version. bc21ace changes no other file.

Co-Authored-By: Claude Code <noreply@anthropic.com>
@SamGu-NRX

Copy link
Copy Markdown
Owner Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Oct 3, 2026 •

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@SamGu-NRX

Copy link
Copy Markdown
Owner Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 75f6c37773

ℹ️ 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".

// Only the scrim's height eases. Reduce Motion asks for no size changes, so it snaps.
.animation(reduceMotion ? nil : Motion.text, value: instruction)
.animation(reduceMotion ? nil : Motion.text, value: reply == nil)
.onChange(of: instruction.title) { detailsShown = false }

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Reset folded details for every new aiming task

At accessibility text sizes, consecutive aiming tasks can have the same title while targeting different stretches—for example, two .aimAtGround tasks both render “Tilt down to show the ground,” with only their detail changing. Because this resets detailsShown only when the title changes, a disclosure opened for the first task remains expanded for the next one, leaving the new task's detail and legend occupying the camera space that this folding behavior is intended to reopen. Reset using the task identity (such as replyTask or the full instruction excluding transient coaching) rather than just instruction.title.

Useful? React with 👍 / 👎.

Comment on lines +1582 to +1583
liveDots.reset()
state.liveDots = .empty

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Invalidate queued dot publications during spatial reset

When a LiDAR integration or anchor correction is already running during a spatial reset, LiveDotsFeed.reset() only queues its field clear behind that work; the older work can first enqueue send back to the main actor. After this method sets .empty and returns, that queued publication is accepted because a spatial reset does not increment generation, so dots in the discarded AR world frame reappear until a new keyframe replaces them. Give dot resets their own generation/epoch and reject publications queued before the reset, as resetAll() already effectively does by changing the scan generation.

Useful? React with 👍 / 👎.

Comment on lines +56 to +58
await Task.detached(priority: .userInitiated) {
MeshMeasurement(facing: mesh.facingSpans(wall: wall, over: span), overheads: mesh.overheadSpans(wall: wall, over: span))
}.value

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Cancel superseded mesh measurements before resending

On a LiDAR phone, if the ground changes while the first packaged upload is measuring its mesh, startUpload() cancels the outer upload task and immediately starts the replacement, but this Task.detached is independent and never receives that cancellation. The withdrawn upload therefore continues all of its per-cell mesh ray casts while the replacement starts another full measurement, retaining both mesh snapshots and competing for CPU until the old calculation finishes. This is precisely the newly supported ground-change-during-measurement path; use cancellable structured work or cancellation checks/serialization so a superseded measurement stops before the resend begins.

Useful? React with 👍 / 👎.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:XXL 1,000+ effective changed lines.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants