Repository navigation
fix(ios): combined app candidate: one-capture uploads, coverage rebuild cache, ground freshness, privacy manifest, AX5 folds, #56 and #49 guidance fixes - #191
Conversation
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>
|
Navigate logical layers of code changes, visualize relationships, and explore their blast radius. Note Reviews pausedIt 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 Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
🧰 Additional context used📚 Code guidelines (1)No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configuration
📒 Files selected for processing (15)
🚧 Files skipped from review as they are similar to previous changes (2)
Included review availability: This review used your included allowance. Your plan provides up to 1 included review per hour; 0 remain after this review. 📝 WalkthroughWalkthroughThe 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. ChangesUpload freshness and guidance
Scene answer validation
Accessibility layouts
Coverage rebuild-view cache
Privacy and platform updates
Diagnostics
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
Suggested reviewers: Merge Risk: ⚪ Minimal · up to The previously identified risk of uploading measurements from the wrong wall is resolved. No actionable merge-blocking issue remains after normal checks. Security Architecture ReviewSecurity architecture risk: 🔵 Low · up to 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 Security review detailsSecurity Blast Radius
Trust Boundaries and Controls
Resilience and Maintainability Implications
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
Full details: Docstring CoverageExplanation 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.)
✨ Finishing Touches 💡 1📝 Generate docstrings 💡
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 |
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
|
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. |
There was a problem hiding this comment.
💡 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".
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>
…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>
…ted ones get" This reverts commit e297d38.
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>
|
@coderabbitai review |
✅ Action performedReview finished.
|
…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>
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>
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>
|
@coderabbitai review |
|
…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>
|
@coderabbitai review |
✅ Action performedReview finished.
|
|
@codex review |
There was a problem hiding this comment.
💡 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 } |
There was a problem hiding this comment.
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 👍 / 👎.
| liveDots.reset() | ||
| state.liveDots = .empty |
There was a problem hiding this comment.
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 👍 / 👎.
| await Task.detached(priority: .userInitiated) { | ||
| MeshMeasurement(facing: mesh.facingSpans(wall: wall, over: span), overheads: mesh.overheadSpans(wall: wall, over: span)) | ||
| }.value |
There was a problem hiding this comment.
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 👍 / 👎.
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:
stats.input_sha256against 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.NSLocalNetworkUsageDescriptionexplains that the app connects to a House Scan server on the local network only when a developer has set one up for testing.feature.acin attempt 2. It addsTapReadinesswith 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.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.packagemeasures 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).saveBundlewrites those inputs and rereads nothing.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.
RebuildViewCacheTestsruns 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 callsettleCurrentTask(), 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_ftand 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.
GroundFreshnessnow 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.foldedcarries those words, and default sizes are unchanged.ScanCopy.withCoachingnow 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:
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,
OpenCameraAimrequired 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, andOpenCameraAimTestsfeeds 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:ScanFolderCleanupreadsscan.zipdates in the app's Cachescapture.started_atIt makes no tracking or collected-data claims; those are App Store privacy answers.
PrivacyManifestTestsfails when shipped Swift uses a category the manifest lacks, or the manifest declares one nothing uses, and pins each reason.Smaller changes
testEndScanQuestionKeepsWalkingOrEndsgets 15 s for its first tap, the same as its second. On run 36841096811 each hittability check took 1-2 s and 5 s ran out.Verification
At 75f6c37:
workflow_dispatch) passed 651 package tests in 92 suites and 57 UI tests with 1 skip and 0 failures. Those UI tests include the every-state audit (779 s),testWholeFlowThroughTheButtons, both tap-helper regressions andInstallerReviewUITests.testWholeFlowThroughTheButtons, the two new regressions and the sevenTapReadinesstests. No UI test has run locally on this tree.At 0b35f65 (test-only on top of b926c92; app, package and project sources are byte-identical):
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 andtestWholeFlowThroughTheButtons.testWholeFlowThroughTheButtonsreported "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.testWholeFlowThroughTheButtonspassed this time, andtestAddSomethingFromTheReviewMarksOnTheCamerafailed instead: XCTest reported "Activation point invalid" forfeature.acon the review, about 4 s after the review appeared, while the helper waited for it to become hittable.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.testSkippingTheGapRequestSendstaps "I can't get there" on a frozen gap request and expects the upload screen. Locally both tests passed on the iPhone 17 Simulator.At b926c92:
viewsFollowTheLookupWhereReadingsMetTheFence.GroundFreshnessUITests(2),FullFlowUITestsincluding the LiDAR replay (6, with the real-recording skip),SourceRestartUITests(1), and the share, rejected-upload and offline screens (3).testPracticeMeterFromReplaywas 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 realpackageand the real mesh measurement behind a gate that holds the measurement while the test changes the source.upload()'s order, with the realGroundFreshnessdeciding. 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.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.
settlingATaskKeepsWhatStalledButANewScanForgetsIt(the same planner state settled versus reset); an eight-case sweep moving one span edge outward by 0.009, 0.010, 0.011 and 0.09 ft, after asked, after skipped and within one answer;aRoundingAllowanceUsedOnceIsNotUsedAgain; equal and contained spans; stricter, equal and lower reaches. Two older expectations encoded the 0.1 ft allowance and were updated: a refused view coming back at 4.9 ft after 4.833334 ft, and a 0.05 ft-wider "jitter" item. Both now ask for evidence progress requires.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-tooDarkin 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
ScanEngineis covered by the pure rule test and review only.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
🤖 Generated with Claude Code
Summary by CodeRabbit