Skip to content

ios: 'I can't get there' moves on to the next requested view (#39) - #49

Open
AidenJohnston wants to merge 11 commits into
t3/ios-mvffrom
t3/gap-skip-continues
Open

AidenJohnston wants to merge 11 commits into
t3/ios-mvffrom
t3/gap-skip-continues

Conversation

@AidenJohnston

@AidenJohnston AidenJohnston commented Sep 26, 2026 •

Copy link
Copy Markdown
Collaborator

Problem. When the check's answer asked for several views ("2 more views to finish"), tapping "I can't get there" on the first one ended the whole loop. skipCurrentGap set automaticGapsStopped for any refused server request, so the upload that followed went straight to the result. The count promised views that never came (field run 2, 3:56.6).

Fix.

  • "I can't get there" now skips only the current view. The upload that follows raises the next view the answer lists. The result shows when no view is left, or after maxAutomaticGaps (5) requests.
  • The choice is made in HouseScanKit (GapPlanner.serverRequests), so it can be unit tested. ScanEngine.automaticGapQueue calls it after each answer, and it keeps the base's guards: the source may capture, and the stretch wasn't refused on the spot check (captureCanSettle). limitEnds is passed through, so ios: past-end requests keep their end; no requests past a wall end (#35, #38) #47's past-end guard also applies to requests raised without a tap.
  • One request per view. A request is skipped when it asks for no view beyond one already raised, skipped, or earlier in the same answer (GapPlan.asksForSameView). That means the same band and kind of need, a span inside the earlier one give or take 0.1 ft, and a reach no farther out. The reach gets no tolerance, because progress needs the requested out_ft itself: ground out to 6.59 ft after 6.50 ft is a new view. A skipped past-end request gets its end back (ios: past-end requests keep their end; no requests past a wall end (#35, #38) #47's settleClearedEnd), so it comes back as the same request.
  • "Show my result" (action.showResult) appears on every request an answer raised, including when one view is left, because the upload after a skip can still bring a new request. It sends this view to installer review, uploads once more and shows the result. At accessibility text sizes it sits below the tape, so the card's "I can't get there" reply keeps the space that passed the contrast audit. It appears and disappears at once, with no fade.
  • "N more views" (state.followUps) comes from the same queue the loop uses: duplicates count once, and the cap applies. The skip hint says "goes on to the next one" with more than one view left, and "checks your scan again" with one. It no longer promises the result.
  • ScanActions.showResultNow() was added. DemoEngine implements it.

Tests.

  • ServerGapTests:
    • each item is asked for once, and a refusal moves on to the next;
    • a refused view that comes back with its span moved by hundredths of a foot isn't raised again, but a farther reach is;
    • 2 to 9 ft after 2 to 8 ft is a new view, and so are 5.10 ft after 4.83 ft and 6.59 ft after 6.50 ft. Each case runs after a raised request, after a skipped one, and as two items in one answer;
    • the past-end guard, and the views-left count.
  • FullFlowUITests. The autopilot writes how each raised request ended (server-requests.json).
    • testCantGetThereEndsWhereThePhoneIs asserts a request is raised after a skipped one.
    • The new testShowMyResultEndsTheRequests taps "Show my result" on the first request and asserts no other is raised before the result.
  • ScreenStatesUITests: gapRequest-followUp must offer action.skipGap and action.showResult, and both must be tappable (at AX5, after scrolling).

Check on a device. Scan a wall and stop short of one end, so the answer asks for two or more views. On the first request, tap "I can't get there". After a short upload the camera should ask for the next view, with the count down by one. Run it again and tap "Show my result" on the first request: the result shows after one upload.

Worth knowing.

  • With a strict reach, a refused view that comes back asking a little farther out is raised again, up to the 5-request cap. "Show my result" is always there as the way out.

Fixes #39

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • Added a “Show my result” option during eligible follow-up requests. Choosing it stops further automatic requests and sends the current request for installer review before showing the result.
    • Added follow-up counts and clearer guidance when skipping a requested view.
  • Bug Fixes
    • Follow-up requests now avoid asking for views already covered, while still allowing later requests for distinct views after a skipped request.

@coderabbitai

coderabbitai Bot commented Sep 26, 2026 •

Copy link
Copy Markdown

Review in Change Stack →

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

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: a54b527a-daee-4194-9a07-1c5de8d4dd86

📥 Commits

Reviewing files that changed from the base of the PR and between 6be087d and 6c3ac62.

📒 Files selected for processing (13)
  • ios/HouseScan/Contract/ScanContract.swift
  • ios/HouseScan/Runtime/Autopilot.swift
  • ios/HouseScan/Runtime/LaunchOptions.swift
  • ios/HouseScan/Runtime/ScanEngine+Actions.swift
  • ios/HouseScan/Runtime/ScanEngine+Export.swift
  • ios/HouseScan/Runtime/ScanEngine.swift
  • ios/HouseScan/UI/Preview/DemoEngine.swift
  • ios/HouseScan/UI/Screens/GapRequestScreen.swift
  • ios/HouseScan/UI/Screens/UploadingScreen.swift
  • ios/HouseScanKit/Sources/HouseScanKit/Guidance/GapPlanner.swift
  • ios/HouseScanKit/Tests/HouseScanKitTests/ServerGapTests.swift
  • ios/HouseScanUITests/FullFlowUITests.swift
  • ios/HouseScanUITests/ScreenStatesUITests.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 scan engine now queues distinct server-requested views, continues to later requests after a homeowner skips one, and tracks the remaining follow-ups. The gap screen adds a “Show my result” action to stop further requests. Autopilot and UI tests cover the updated flow.

Changes

Server follow-up requests

Layer / File(s) Summary
Select distinct server requests
ios/HouseScanKit/Sources/HouseScanKit/Guidance/GapPlanner.swift, ios/HouseScanKit/Tests/HouseScanKitTests/ServerGapTests.swift
GapPlanner selects ordered requests that differ from previously asked or skipped views. Tests cover limits, span and reach comparisons, overhead requests, and end limits.
Manage the follow-up queue
ios/HouseScan/Contract/ScanContract.swift, ios/HouseScan/Runtime/ScanEngine.swift, ios/HouseScan/Runtime/ScanEngine+Actions.swift, ios/HouseScan/Runtime/ScanEngine+Export.swift
ScanEngine builds and counts the request queue. Skipping a request no longer stops later requests. showResultNow() stops the queue and sends the current request for installer review before upload.
Display follow-ups and actions
ios/HouseScan/UI/Screens/GapRequestScreen.swift, ios/HouseScan/UI/Screens/UploadingScreen.swift, ios/HouseScan/UI/Preview/DemoEngine.swift, ios/HouseScanUITests/ScreenStatesUITests.swift
The screens use the engine’s follow-up count. The gap screen adds “Show my result,” adapts its placement for accessibility text sizes, and varies the skip hint by remaining requests. Demo and screen-state tests cover the related behavior.
Exercise the request flow
ios/HouseScan/Runtime/LaunchOptions.swift, ios/HouseScan/Runtime/Autopilot.swift, ios/HouseScanUITests/FullFlowUITests.swift
Autopilot can select “Show my result” and record server request outcomes. Full-flow tests check the recorded actions for skip and show-result scenarios.

Priority: ➖ Normal

Estimated code review effort: 3 (Moderate) | ~25 minutes

Change: Bug fix · Severity of issue fixed: Medium

Sequence Diagram(s)

sequenceDiagram
  participant ScanEngine
  participant GapPlanner
  participant GapRequestScreen
  participant Homeowner
  ScanEngine->>GapPlanner: Select ordered distinct requests
  GapPlanner-->>ScanEngine: Return capturable request queue
  ScanEngine->>GapRequestScreen: Provide current request and follow-up count
  Homeowner->>GapRequestScreen: Tap Show my result
  GapRequestScreen->>ScanEngine: Call showResultNow()
  ScanEngine->>ScanEngine: Stop requests and send current request for installer review
Loading

Suggested reviewers: samgu-nrx

Merge Risk: ⚪ Minimal · up to 6c3ac

The change lets a refused follow-up view lead to the next requested view and adds a "Show my result" action. No actionable merge-blocking issue remains after review.

Security Architecture Review

Security architecture risk: 🔵 Low · up to 6c3ac

The change appears bounded to the current scan and preserves request limits, capture eligibility, and installer review when the homeowner stops. No introduced security vulnerability was established. Failure recovery and the isolation of automated test controls could not be fully verified.

Retained concerns
No architecture-level concerns identified.

Security review details

Security Blast Radius

  • inferred — The demonstrated changed scope is the current scan's follow-up sequence, capture guidance, result progression, and automated request recording. The supplied evidence does not establish additional tenant, service, credential, or infrastructure authority; incomplete relationship coverage prevents treating that as a comprehensive exposure assessment.

Trust Boundaries and Controls

  • inferred — Server-returned missing-evidence requests influence local capture progression, but the queue summary retains tracking, capture-availability, coverage, capturability, stop-state, and remaining-request-budget controls. This is counterevidence to an unrestricted capture loop, not verification of all server-input validation.
  • observed — The automated show-result option defaults to false and is enabled by a launch argument. Request outcome recording writes server-requests.json only when autopilotGate is set. These gates are described in the changed ranges, but the complete activation boundary for automated execution was not verified.

Resilience and Maintainability Implications

  • observed — Earlier source inspection establishes that waitForGate returns without suspending unless both autopilot and autopilotGate are enabled. Thus, the queue's before-and-after evaluation at this gate does not itself establish a production interleaving hazard when gating is disabled. Upload recovery and obsolete completion handling remain unresolved.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 68.42% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 38 functions across 13 files. 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 clearly identifies the primary behavior change: refusing one requested view now advances to the next requested view.
Description check ✅ Passed The description is detailed and covers the problem, behavior changes, rationale, UI changes, tests, device verification, and known risks. It does not use all template headings and omits the checklist …
Linked Issues check ✅ Passed Issue #39 requires the app to avoid promising unavailable views after a refused server request. The PR keeps an ordered queue of eligible, distinct follow-up requests, raises the next request after a …
Out of Scope Changes check ✅ Passed The changed contract, engine, planner, UI, demo flow, autopilot option, and tests support issue #39. The planner changes implement request selection and deduplication. The UI and accessibility changes…
  • 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.

@github-actions github-actions Bot added the size:L 100-499 effective changed lines. label Sep 26, 2026
After the check's answer asks for several views, "I can't get there"
on one of them used to stop the loop: the next upload showed the
result, so "2 more views to finish" delivered only one view.

Now it skips only the current view: the next answer raises the next
item it lists, and the result shows once none is left. Each item is
raised at most once per pass (GapPlanner.nextServerRequest); a
past-end item is known by its side, since raising it clears the end
and it comes back with a different request. A new "Show my result"
button on the follow-up request stops early, and the skip button's
VoiceOver hint says where it leads.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@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 house-scanning-master 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: e4ce503160

ℹ️ 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 on lines +93 to +96
switch followUps {
case 0: "Skips this view. An installer will look at this part instead."
case 1: "Skips this view and shows your result. An installer will look at this part instead."
default: "Skips this view and goes on to the next one. An installer will look at this part instead."

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 Exclude already-raised views from the remaining count

When a past-end request is skipped, clearing that end makes the same server item produce a new GapPlan, so presentation(of:) still marks it capturable even though nextServerRequest correctly excludes it via asksForSameView. Consequently, on the actual last request followUps can remain 2: the eyebrow claims two views remain and this new hint says skipping will go to another view, but the subsequent upload goes directly to the result. Derive the displayed count and hint from the same asked-view filtering used by the planner (and account for the automatic-request cap) so the UI describes the transition that will actually occur.

Useful? React with 👍 / 👎.

@SamGu-NRX

Copy link
Copy Markdown
Owner

Read-only review of #49 found two integration/copy risks. First, #47 changes GapPlanner.plan to take limitEnds, but #49’s new nextServerRequest calls plan without it (GapPlanner.swift:297-303). If both branches land, a ground request beyond a marked limit end can be reported capturable on the result and then silently skipped by the automatic selector. Pass map.limitEnds through the selector and test that combined path before integration. Second, GapRequestScreen.followUps still counts all capturable items (:83-97), while the selector deduplicates views and stops at five requests. Two items asking for the same view can promise "2 more views" and "goes on to the next one" yet show the result after Skip; six items can exceed the cap. The PR body acknowledges a past-end count variant, but this affects general duplicates and the cap too. Derive the promise from eligible remaining requests or avoid a numeric/next-view claim.

@SamGu-NRX

Copy link
Copy Markdown
Owner

One point beyond the caretaker's two, from reading e4ce503:

A refused view can come back with slightly different numbers. asksForSameView matches band requests on exact span_ft and out_ft, and skipped.contains(plan) compares plans exactly. After "I can't get there" the phone uploads again, and the server recomputes each request from the new scene. The skipped stretch goes to review, the spot or the error margins can move, and the same stretch comes back as, say, [2.41, 7.9] instead of [2.4, 7.9]. Neither check matches it, so the homeowner is asked for the view they just said they can't reach, until the five-request cap. Match a band request as the same view when the band is the same and its span is mostly inside one already asked or skipped (for example, at least 80% of it overlaps), or treat a nearby out_ft as the same, not only an equal one. Add a test where the second answer's span moves by a few hundredths of a foot.

The caretaker's points still stand: pass limitEnds through nextServerRequest once #47 lands, and derive the "N more views" promise from what the selector will actually raise. The branch also needs t3/ios-mvf merged in for the schema check. The rest reads correctly: "Show my result" as the only stop, the skip hint by count, and the demo stub.

@SamGu-NRX

Copy link
Copy Markdown
Owner

Current head e4ce503 failed its iOS Simulator check (run 36276179058). This is not one failure: testCantGetThereEndsWhereThePhoneIs asserts a left end near -16.4 ft but got -19.0 ft; testFullFlowFromReplay failed to terminate the app before relaunch; and the accessibility audit reports contrast failure on action.skipGap in gapRequest-followUp-AX5. Package tests and app build passed. Please investigate the assertion and contrast separately from the likely Simulator lifecycle failure, then rerun the current head. The two code-review findings I posted earlier remain open.

@AidenJohnston

Copy link
Copy Markdown
Collaborator Author

Pre-CI check

This is the cause of two of the three failures in run 36276179058. Both come from this branch's change, so rerunning e4ce503 will fail again.

1. testCantGetThereEndsWhereThePhoneIs left end at -19.0 ft (FullFlowUITests.swift:92). The bundled SampleResult.json lists two items in its missing evidence: a ground band at 3 to 5.5 ft, then a left past_end. On t3/ios-mvf, "I can't get there" on the ground request sets automaticGapsStopped, so the result shows and the left past-end is never raised. On this branch the loop goes on. nextServerRequest raises the left past_end, and beginServerGap (ScanEngine.swift:1417-1427) calls clearEnd(.left). The autopilot then says "I can't get there" again. skipCurrentGap never puts the end back, so the scene exports a left end the phone never reached, 19 ft out instead of 16.4 ft.

Fix: when a past-end request is skipped, the homeowner couldn't get past the end, so the end should stay where it was. In beginServerGap, save the end's position, endKinds[side] and markTimes[.end(side)] before clearEnd. In skipCurrentGap, when pastEndSide != nil and the view wasn't taken, restore them before afterGapResolved() sets pastEndSide to nil. This also matches the test's intent that the ends stay within a keyframe of the phone. Changing the test's numbers instead would hide a real bug: a homeowner who can't walk past an end would lose that end.

2. Contrast failure on action.skipGap in gapRequest-followUp-AX5 (ScreenStatesUITests.swift:301). The new "Show my result" button (GapRequestScreen.swift:48-58) adds a row to the chrome's stack on the follow-up state. This is the only layout change on that screen, and the base passes the same state. At AX5 the extra row most likely pushes the instruction card's "I can't get there" reply up over the camera image, where the audit fails its contrast. The other camera screens change their layout at accessibility sizes (typeSize.isAccessibilitySize in WallWalkScreen.swift:239 and MarkFeaturesScreen.swift:206). A fix along those lines: at accessibility sizes, put the two ways out side by side or in one row with the card, or give the stack an opaque background. The gapRequest-followUp state is launched with -uiDemoPhase gapRequest -uiDemoFollowUp, so you can check it on a Simulator at AX5 in seconds.

3. testFullFlowFromReplay failed to terminate the app (FullFlowUITests.swift:204). This also happened on #37 (run 36276096175) without this branch's changes, so it looks like a Simulator lifecycle problem after the LiDAR replay test, not something here.

@AidenJohnston

Copy link
Copy Markdown
Collaborator Author

Pre-CI check (follow-up)

#47 (t3/past-end-requests, 99e914c) already has the fix for the -19.0 ft left end. Its settleClearedEnd(), called from afterGapResolved(), puts a cleared end back with its kind and mark time when a past-end request ends without the end marked again. So coordinate rather than write a second fix. Either wait for #47 to land and merge t3/ios-mvf, or reuse its clearedEnd approach.

The two branches will conflict in ScanEngine.nextAutomaticGap. #47 passes limitEnds: map.limitEnds to gapPlanner.plan(for:leftEnd:rightEnd:limitEnds:), and this branch replaces that loop with GapPlanner.nextServerRequest. When you resolve it, add a limitEnds: Set<WalkSide> parameter to nextServerRequest and pass it through to plan(for:). Otherwise #47's reachesPastEnd guard is silently dropped for the requests raised without a tap. The default limitEnds: [] compiles either way, so no test would catch this.

@AidenJohnston

Copy link
Copy Markdown
Collaborator Author

Pre-CI check (after #47 merged)

t3/ios-mvf is now d02cc11, with #47 merged. This branch conflicts with it in ScanEngine.swift, GapPlanner.swift and ServerGapTests.swift, so CI won't run on it until you merge the base. Two of the hunks have a resolution that builds wrong or fails CI:

  1. ScanEngine.swift, the stored properties after pastEndSide (about line 82). Keep the base's private var clearedEnd: (s: Float, kind: EndKind, t: Double?)? next to this branch's automaticGaps: [PlacementMissingEvidence] and the new automaticGapsStopped comment. If you take this branch's side, clearedEnd goes, and settleClearedEnd() and beginServerGap stop compiling. settleClearedEnd() is also what fixes this branch's testCantGetThereEndsWhereThePhoneIs failure (left end at -19.0 ft), so keeping it matters beyond the build.
  2. ScanEngine.swift, nextAutomaticGap (about line 1448). Keep gapPlanner.nextServerRequest(...), but pass limitEnds: map.limitEnds through. Add a limitEnds: Set<WalkSide> = [] parameter to nextServerRequest in GapPlanner.swift, and use it in its call to self.plan(for:leftEnd:rightEnd:limitEnds:). Without it, ios: past-end requests keep their end; no requests past a wall end (#35, #38) #47's reachesPastEnd guard is dropped for the requests raised without a tap, and nothing fails, because the parameter defaults to empty.
  3. GapPlanner.swift (about line 317) and ServerGapTests.swift: both sides add new code at the end, so keep both. endAfterPastEnd from the base is called by settleClearedEnd.

After the merge, the sample result's left past-end request, skipped by the -autopilotCantGetThere autopilot, puts the left end back where it was. That should bring the left end back to about -16.4 ft. The AX5 contrast finding on action.skipGap in gapRequest-followUp-AX5 is separate and still open.

Resolves the GapPlanner/ScanEngine conflicts with #47: the selector now
passes limitEnds, matches requests by view with a tolerance (same band and
need, reach within 0.1 m, 80% span overlap), and publishes how many views
it will actually raise as state.followUps. 'Show my result' shows only
when more than one view is left.

Co-authored-by: Cursor <cursoragent@cursor.com>
@SamGu-NRX

Copy link
Copy Markdown
Owner

My point at e4ce503 is addressed at ce90112. GapPlan.asksForSameView now matches the same band with a need within 0.1 and at least 80% of the span overlapping, and both asked and skipped go through it, so a refused view that comes back with slightly different numbers isn't raised again. limitEnds now reaches the selector too. I have nothing further; the caretaker's review of this head still gates the merge.

@SamGu-NRX

Copy link
Copy Markdown
Owner

@AidenJohnston Independent source review of ce90112202265f211509e5f73cdbe178f1894506: the count, limitEnds forwarding, end restoration and small numerical-change deduplication fixes are present. One acceptance gap remains before I can clear this head.

Please exercise the multi-view follow-up at AX5 with both action.skipGap and action.showResult visible and hittable, and run the accessibility audit on that state. GapRequestScreen now shows the second button only for followUps > 1, but DemoEngine.swift:485 sets the existing -uiDemoFollowUp fixture to 1. ScreenStatesUITests.swift:43,63 therefore no longer exercises the extra row that was present when the old head failed contrast on action.skipGap. A green rerun of that one-view fixture will not settle the reported multi-view layout concern. Keep the one-view case too.

This is a verified coverage gap, not a claim that contrast still fails at this head. I have not run a local Simulator; current-head CI is still running.

@SamGu-NRX

SamGu-NRX commented Sep 27, 2026 •

Copy link
Copy Markdown
Owner

@AidenJohnston Two further findings are confirmed by source review at ce90112202265f211509e5f73cdbe178f1894506.

  1. GapPlanner.swift:364-369 drops materially larger evidence requests as duplicates. After ground span 2–8 ft, a new 2–9 ft span overlaps by 6/7, so it passes the 80% test and serverRequests suppresses the extra foot. Likewise a 5.10 ft reach after 4.83 ft differs by 0.0823 m, below Need.isNear's 0.1 m tolerance, so the stricter requirement is suppressed. This applies to already asked plans and even earlier requests in the same response, not only refusals. presentation(of:) also uses this matcher for skipped requests, which can remove the larger request's capture action after a skip. Preserve materially expanded spans and stricter reaches while suppressing numerical jitter. Please test both examples through serverRequests, including two items in one answer. These are arithmetic traces of the source predicates, not a Swift/device test run.

  2. The stop action disappears at one remaining view, but this is only the current answer's queue. After skipping that view, skipCurrentGap leaves automaticGapsStopped false; the next upload can return a new non-overlapping view and automaticGapQueue raises it. Meanwhile GapRequestScreen hides Show my result for followUps == 1 and says skipping "shows your result". Keep an unconditional way to stop follow-up requests and avoid promising that the current one-view answer is the final upload. A focused case is answer A with one ground request, then answer B with a new wall request after Skip; the explicit stop path must still reach the result.

The five-request cap itself is enforced by the inspected queue code; I found no sixth-request issue. The multi-view AX5 coverage request in comment 5851111550 remains open.

Build 4.1 field report #82 describes seven rapid refusal taps to finish, including taps that set unexplored ends. This supports the persistent-stop acceptance requirement; it is evidence from beta 45017ba, not a reproduction on this PR head. Keep #82 as the field issue rather than duplicating it. Also merge current t3/ios-mvf de04c3b or later to retain the shared schema-fetch correction before final CI.

@AidenJohnston

Copy link
Copy Markdown
Collaborator Author

Merged t3/ios-mvf and addressed the review (ce90112, iOS build green)

Left end at -19.0 ft. #47 is now in the base, and its settleClearedEnd puts a past-end request's end back when the homeowner says "I can't get there". In the replay, the left past-end request is now raised, skipped, and its end restored to where the walk put it (about -16.4 ft), and then the result shows. The test's numbers are unchanged.

#47 interaction. #47 had already merged into t3/ios-mvf, so merging the base was enough and I didn't merge t3/past-end-requests separately. GapPlanner.serverRequests / nextServerRequest now take limitEnds and pass it to plan(for:), so #47's reachesPastEnd guard also applies to requests raised without a tap. A test covers this.

A refused view coming back with slightly different numbers. Asked and skipped requests are now compared by view (GapPlan.asksForSameView): same band and kind of need, reach within 0.1 m, and at least 80% of the span inside the earlier one. This replaces exact equality in the selector and in the export's capturable check. A met past-end request moves its end on 2 m, so the next one still counts as a new view. The test's second answer moves the span from 2.4 to 2.41 ft and the reach from 4.83 to 4.9 ft.

"N more views" promise. The engine publishes state.followUps from the same selector the automatic loop uses. Duplicate items count once, the five-request cap applies, and the count is zero after "Show my result". The upload and gap screens now read that number instead of counting capturable items.

AX5 contrast on action.skipGap. The CI screenshot shows the card is opaque, and "I can't get there" was cut off by the bottom edge at AX5. The test's reveal scroll fixed that on base but not with the extra "Show my result" row. With one view left, both buttons lead to the result, so "Show my result" now appears only when more than one view is left. The one-view follow-up state then renders as it does on base.

testFullFlowFromReplay's "Failed to terminate" was a Simulator lifecycle failure and didn't recur.

@SamGu-NRX

Copy link
Copy Markdown
Owner

@AidenJohnston Sam is moving ahead with a test beta while main PRs stay open. Please prioritize two small fixes on your #49 branch so the beta can test refusal progression meaningfully:

  1. Keep "Show my result" available for every server follow-up, including a one-item answer; remove the promise that one item guarantees the result next.
  2. Preserve newly requested coverage and stricter reach when deduplicating. Add regressions for 2–9 ft after 2–8 ft and 5.10 ft reach after 4.83 ft. Suppress numerical jitter only when it adds no required evidence.

Please merge current t3/ios-mvf while preserving its end restoration, limit-end forwarding and three-file schema-fetch guard, then post the fix SHA. The beta lead can test that concrete head; branch writes remain yours. The multi-view AX5 case stays in beta QA. Current ce90112 is not independently cleared, and this request does not ask for main integration or a blind CI retry.

Stacks this branch on PR #49 at its current head, on top of the current
t3/ios-mvf. The merge had no conflicts.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…very follow-up (#39)

The two open review findings on #49:

- GapPlan.asksForSameView suppressed a request whose span was 80 %
  inside an earlier one and whose reach was within 0.1 m, so 2-9 ft after
  2-8 ft, or ground out to 5.10 ft after 4.83 ft, was dropped as a
  duplicate. It now matches only a request whose span lies inside the
  earlier one and whose reach is no more than the earlier one's, each
  give or take 0.1 ft. Tests cover both cases through serverRequests,
  after a raised and a skipped request, and as two items in one answer.
- "Show my result" was hidden with one view left, and the skip hint said
  skipping shows the result. The next upload can still bring a new
  request, so the button now shows on every server follow-up and the
  one-view hint says the scan is checked again.

Also deletes the empty ios/ServerGapTests.swift that #49 added by
mistake.

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

Copy link
Copy Markdown
Owner

@greptileai

AidenJohnston and others added 2 commits September 27, 2026 05:50
Conflicts: the export's capturable check takes the base's capturable(item)
closure, and the automatic queue keeps GapPlanner.serverRequests with the
base's sourceState.mayCapture guard and captureCanSettle filter.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Brings the two #39 fixes Sam asked for on this branch: a follow-up that
asks for more span or reach is a new view (asksForSameView), and "Show my
result" on every server follow-up. #109 carries the same commit, so the two
branches share it.

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

greptile-apps Bot commented Sep 27, 2026 •

Copy link
Copy Markdown

RetriggerConfidence Score: 4/5

[Medium risk] Adds follow-up request handling to the gap review flow.

The PR should not merge until request deduplication preserves spans the server still considers unmet.

Fix All in Claude CodeFindings

  1. P1 Unmet views can be discarded ▶
  2. P2 Engine transition remains untested ▶
Fix with agent prompt
### Issue 1
ios/HouseScanKit/Sources/HouseScanKit/Guidance/GapPlanner.swift:379
If a later server answer extends a previously raised or skipped span by 0.09 ft, this 0.1 ft tolerance treats it as the same view. But `GapPlanner.progress` counts an uncovered gap of at least 0.01 ft as missing. `serverRequests` can therefore discard a request the server still needs and show the result without offering that view. The deduplication tolerance needs to agree with what settles a request.

### Issue 2
ios/HouseScanKit/Tests/HouseScanKitTests/ServerGapTests.swift:undefined-251
This test checks planner output, but not the reported failure path: tapping “I can’t get there,” uploading, and entering the next request instead of the result. It also does not test whether “Show my result” stops that transition. A change to the engine’s stopping flag or upload path could therefore bring back the bug while these assertions still pass.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.
Summary

The PR lets “I can't get there” advance to the next server-requested view, adds a separate “Show my result” exit, and derives follow-up counts from a deduplicated request queue.

  • The deduplication span tolerance can suppress a request that the server still considers unmet.
Diagram
%%{init: {'theme': 'neutral'}}%%
flowchart LR
  A[Upload receives answer] --> B[Build automatic request queue]
  B --> C{Request counted as same view?}
  C -- No --> D[Ask for view]
  C -- Yes --> E[Skip request]
  D --> F[Capture or skip, then upload]
  F --> A
  E --> G[Show result if queue is empty]
Loading

Reviews (1) · Last reviewed commit: "ios: UI tests for skip moving on and "Sh..."

Comment on lines +375 to +379
case let (.groundOut(a), .groundOut(b)), let (.walkOut(a), .walkOut(b)), let (.wallUp(a), .wallUp(b)):
return abs(a - b) <= tolerance
case let (.overhead(a), .overhead(b)):
guard let a, let b else { return a == nil && b == nil }
return abs(a - b) <= tolerance

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Distinct reaches treated as identical When the server asks for two views of the same span at different required reaches, such as 6.5 ft and 6.6 ft, this 0.1 m tolerance treats them as the same view. Capturing the first may not satisfy the second, but serverRequests drops it and can show the result while evidence is still missing. If the first view was skipped, the same comparison also disables the second view’s capture action on the result.

Prompt To Fix With AI
This is a comment left during a code review.
Path: ios/HouseScanKit/Sources/HouseScanKit/Guidance/GapPlanner.swift
Line: 375-379

Comment:
**Distinct reaches treated as identical** When the server asks for two views of the same span at different required reaches, such as 6.5 ft and 6.6 ft, this 0.1 m tolerance treats them as the same view. Capturing the first may not satisfy the second, but `serverRequests` drops it and can show the result while evidence is still missing. If the first view was skipped, the same comparison also disables the second view’s capture action on the result.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Fix in Claude Code Fix in Codex

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Fixed in 88bca30. The 0.1 ft tolerance now applies to the span only. A reach may be no farther out than the earlier one: compared on the server's out_ft when both requests have one, else in meters within float noise. ServerGapTests.aSlightlyFartherReachIsANewView covers 6.59 ft after 6.50 ft through serverRequests, after a raised request, after a skipped one, and as two items in one answer.

/// Issue #39: the answer asks for two views and the homeowner can't get to the first. The
/// next answer, which still lists it, moves on to the second instead of raising the first
/// again or stopping; once both were raised, none is left and the result shows. The skipped
/// past_end request gets its end back (`ScanEngine.settleClearedEnd`), so it comes back as

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 Engine transition remains untested This test checks planner output, but not the reported failure path: tapping “I can’t get there,” uploading, and entering the next request instead of the result. It also does not test whether “Show my result” stops that transition. A change to the engine’s stopping flag or upload path could therefore bring back the bug while these assertions still pass.

Prompt To Fix With AI
This is a comment left during a code review.
Path: ios/HouseScanKit/Tests/HouseScanKitTests/ServerGapTests.swift
Line: 251

Comment:
**Engine transition remains untested** This test checks planner output, but not the reported failure path: tapping “I can’t get there,” uploading, and entering the next request instead of the result. It also does not test whether “Show my result” stops that transition. A change to the engine’s stopping flag or upload path could therefore bring back the bug while these assertions still pass.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Fix in Claude Code Fix in Codex

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Covered in 6c3ac62 with UI tests that run the engine. The autopilot writes how each request the server's answers raised ended (server-requests.json in the gate folder). FullFlowUITests.testCantGetThereEndsWhereThePhoneIs asserts a request is raised after a skipped one. The new testShowMyResultEndsTheRequests taps "Show my result" on the first request and asserts no other is raised before the result. Both passed on the Mac mini at 6c3ac62.

@greptile-apps

This comment has been minimized.

Ported from #109 (a44780a). With the button back on every follow-up, the
one-view state has the extra row that failed the reply's contrast at AX5
(CI run 36276179058). At accessibility sizes the button now goes below the
tape, so the space under the card matches the layout that passed. The
screen-state test asserts the follow-up's two controls can be tapped, after
scrolling to them at AX5.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
AidenJohnston added a commit that referenced this pull request Sep 27, 2026
…result

Brings #49's merge of the current base. Conflicts:
- ScanEngine.upload: the #31 resultHold wait, then the base's presentAnswer
  (spot check) instead of going straight to the result. DemoEngine the same.
- ResultScreen: the base's redesigned layout; its unseen-side footnote now
  shows #83's unseenEnd copy. ScanCopy.unseenSide is unused again and removed.
- ScreenStatesUITests: expectations hold a list per state, so result-review
  checks both the base's headline and #83's unexplored-end copy.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
AidenJohnston added a commit that referenced this pull request Sep 27, 2026
…result

Conflict: ScreenStatesUITests expectations take the base's new spot-check
entries, kept as a list per state, with #75's ground-out range and #83's
unexplored-end copy.

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

Copy link
Copy Markdown
Owner

@AidenJohnston At e576190, the reach-dedup finding remains for smaller increases. GapPlanner367 and392-397 allow another0.1ft: for identical spans,6.59ft after6.50ft is treated as already asked, while progress160-166 requires the exact requested reach. The stricter request can still disappear, or its capture action stays disabled after a skip. The reduced tolerance fixes larger differences, not this case. Please separate harmless span jitter from genuinely stricter reach, or provide evidence that the upstream contract treats this difference as equivalent. This affects evidence collection, not a claim that the server returns PASS. #109 f2309cb inherits it. Empty test file removal is verified; engine stop/skip transition coverage remains separate.

@SamGu-NRX

Copy link
Copy Markdown
Owner

@codex review

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 27, 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-09-27T11:10:17.328994Z e576190 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.

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. What shall we delve into next?

Reviewed commit: e57619015a

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

@AidenJohnston

Copy link
Copy Markdown
Collaborator Author

Updated for review: head e576190

Base merges. b25dc4f merged t3/ios-mvf at e62be65. Two files conflicted:

  • ScanEngine+Export.swift: I took the base's capturable(item) closure and gave its settles check asksForSameView.
  • ScanEngine.automaticGapQueue: it keeps GapPlanner.serverRequests and adds the base's sourceState.mayCapture guard and captureCanSettle filter.

The base's end restoration and limitEnds forwarding are kept. e576190 merged t3/ios-mvf at 29ebe84 with no conflicts.

Review items

  • Larger requests dropped as duplicates (GapPlanner.swift:364-369, ce90112): fixed by 660decd, a merge of ios: gap-loop follow-ups, upload ticks, the nearer unseen end and request stretches (#39, #31, #83, #75) #109's commit 9b5348b, so both branches share one commit. A request is the same view only if its span lies inside the earlier span and its reach is no larger, each within 0.1 ft. Tests: 2 to 9 ft after 2 to 8 ft, and a 5.10 ft reach after 4.83 ft, each through serverRequests after a raised request, after a skipped one, and as two items in one answer.
  • "Show my result" hidden with one view left, and the hint promising the result: fixed in the same commit 9b5348b. The button now shows on every server follow-up. The one-view hint now reads "Skips this view and checks your scan again".
  • AX5 contrast on action.skipGap in gapRequest-followUp: a041f18 ports ios: gap-loop follow-ups, upload ticks, the nearer unseen end and request stretches (#39, #31, #83, #75) #109's a44780a. At accessibility sizes "Show my result" moves below the tape. The screen-state test now checks that both controls can be tapped: in place at the default size, after scrolling at AX5. With the button on every follow-up, the one-view fixture now shows the same extra row that the multi-view case had. Sam's latest comment leaves the separate multi-view AX5 fixture to beta QA.
  • Left end at -19.0 ft instead of -16.4 ft: fixed by ios: past-end requests keep their end; no requests past a wall end (#35, #38) #47's settleClearedEnd, which comes in with the base. testCantGetThereEndsWhereThePhoneIs passed on the Mac mini at e576190.
  • Codex: remaining count after a past-end skip (GapRequestScreen.swift:97): already addressed at ce90112. state.followUps comes from the same selector the loop uses.

CI

  • GitHub iOS build at e576190: pass.
  • Mac mini at e576190:
    • Package tests: 453 passed, with -warnings-as-errors.
    • A targeted audit of every gapRequest* state (with AX5), uploading-followUp and result-review, plus testCantGetThereEndsWhereThePhoneIs. In the first run, gapRequest-followUp-AX5 failed contrast on action.skipGap. A rerun of the same head passed. a041f18 and ios: gap-loop follow-ups, upload ticks, the nearer unseen end and request stretches (#39, #31, #83, #75) #109's f2309cb also passed this state. Both runs made the same 145.7 pt reveal drag and got different results, so it looks intermittent. It's worth watching.
    • testFullFlowFromReplay failed there on the known screen.wallWalk contrast flake.

Not claiming review-clear.

🤖 Generated with Claude Code

AidenJohnston and others added 3 commits September 27, 2026 08:10
Conflict: upload() takes the base's snapshot loop (AnswerFreshness). After
it, the answer sets state.followUps from automaticGapQueue before .done, and
the next request is automaticGapQueue(result).first.

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

asksForSameView let a reach move 0.1 ft like the span, so ground out to
6.59 ft after the same span out to 6.50 ft counted as the same view and was
never raised, and after a skip its capture action stayed off, though
progress needs the requested out_ft itself. The span keeps the 0.1 ft
tolerance; a reach may be no farther than the earlier one's, compared on the
server's out_ft when both have one, else in meters within float noise.

Tests: 6.59 ft after 6.50 ft through serverRequests after a raised and a
skipped request and as two items in one answer; the refused-view test now
moves the span only, and a farther reach after a refusal is raised.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The autopilot writes how each request the server's answers raised ended
(server-requests.json in the gate folder). testCantGetThereEndsWhereThePhoneIs
now asserts a request is raised after a skipped one, and the new
testShowMyResultEndsTheRequests taps "Show my result" on the first
(-autopilotShowResult) and asserts no other request is raised before the
result. "Show my result" appears and goes at once (.identity), like the
card's reply, so no faded frame meets the contrast audit.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
AidenJohnston added a commit that referenced this pull request Sep 27, 2026
Brings the base at 6be087d through #49. Conflicts:
- ScanEngine.upload: the base's snapshot loop (AnswerFreshness), with #31
  ported into it: analyzingSince resets on each attempt before submit and
  is set when its bytes are sent, the analyzingMinimum wait comes after the
  loop before .done, and resultHold before presentAnswer, both in the base's
  guard (try? await Task.sleep(...)) form.
- ScreenStatesUITests: the base's wallWalk-fullLegend expectation, in the
  list format.

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

Copy link
Copy Markdown
Collaborator Author

Updated for review: head 6c3ac62

@SamGu-NRX, on your 11:06 finding (reach dedup):

  • Fixed in 88bca30. asksForSameView keeps the 0.1 ft tolerance for the span only. A reach may be no farther out than the earlier one. When both requests carry the server's out_ft, it compares those exactly, as progress does; otherwise it compares in meters, allowing only float noise.
  • New test aSlightlyFartherReachIsANewView: 6.59 ft after 6.50 ft, through serverRequests after a raised request, after a skipped one, and as two items in one answer.
  • Updated tests:
    • aRefusedViewWithSlightlyDifferentNumbersIsNotRaisedAgain now moves only the span, and asserts that a farther reach (4.9 ft after 4.833334 ft) is raised.
    • aRequestAskingForMoreIsANewView's jitter case uses a nearer reach.
  • Result screen: it uses the same check for skipped requests, so the capture action stays on for the stricter request.

Other changes

  • c0b3e37 merges t3/ios-mvf at 6be087d. upload() takes the base's snapshot loop. After it, state.followUps is set from automaticGapQueue before .done, and the next request is automaticGapQueue(result).first.

  • 6c3ac62 adds tests for Greptile's engine-flow P2 (thread replied), using a new autopilot record, server-requests.json:

    • testCantGetThereEndsWhereThePhoneIs asserts that a skip raises the next request;
    • testShowMyResultEndsTheRequests asserts that "Show my result" on the first request ends them.

    "Show my result" now uses .transition(.identity), like the card's reply.

  • Description rewritten to match the code.

CI at 6c3ac62

  • GitHub: iOS build passed.
  • Mac mini: passed.
    • 467 package tests.
    • testCantGetThereEndsWhereThePhoneIs and testShowMyResultEndsTheRequests.
    • A targeted audit of the gapRequest* states, uploading-followUp and result-review, with gapRequest-followUp and its AX5 variant audited three times.

Not claiming review-clear.

🤖 Generated with Claude Code

@SamGu-NRX

Copy link
Copy Markdown
Owner

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Sep 30, 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

@greptileai review

Please review the current head 6c3ac6252f9738e94b6ab10b16f0cfc4ee4a5d0d against this PR's actual base. Catch-up review requested by Sam after enabling BaseScanning in Greptile.

/// (0.03 m), meters. Numerical jitter from the server working the request out again is a few
/// hundredths of a foot; a guess, not measured. This applies to the span only: a reach gets
/// no allowance beyond float noise (`reachNoise`).
public static let sameViewTolerance: Float = 0.03048

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Unmet views can be discarded

If a later server answer extends a previously raised or skipped span by 0.09 ft, this 0.1 ft tolerance treats it as the same view. But GapPlanner.progress counts an uncovered gap of at least 0.01 ft as missing. serverRequests can therefore discard a request the server still needs and show the result without offering that view. The deduplication tolerance needs to agree with what settles a request.

Prompt To Fix With AI
This is a comment left during a code review.
Path: ios/HouseScanKit/Sources/HouseScanKit/Guidance/GapPlanner.swift
Line: 379

Comment:
**Unmet views can be discarded**

If a later server answer extends a previously raised or skipped span by 0.09 ft, this 0.1 ft tolerance treats it as the same view. But `GapPlanner.progress` counts an uncovered gap of at least 0.01 ft as missing. `serverRequests` can therefore discard a request the server still needs and show the result without offering that view. The deduplication tolerance needs to agree with what settles a request.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Fix in Claude Code Fix in Codex

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

@AidenJohnston Caretaker check at 6c3ac62: confirmed, and new in this PR (the base compared plans exactly). Ask for 2…8 ft, and the next answer asks for 2…8.09 ft at the same reach. asksForSameView treats them as the same view within sameViewTolerance = 0.1 ft (GapPlanner.swift:379-399), so serverRequests drops the second request. Meanwhile progress counts any uncovered stretch of 0.01 ft or more as missing (lines 236-249). With nothing else queued, ScanEngine.swift:1528-1541 shows the result, and after a skip ScanEngine+Export.swift:193-200 marks the request as not capturable. Evidence gets lost; it doesn't turn UNSURE into PASS. Your 88bca30 reach fix holds; this is the span side.

Please make the span allowance agree with progress: an extension of 0.01 ft or more is a new view. Use requestedSpanFt when both plans have it. Add ServerGapTests cases just below, at and above 0.01 ft, for a raised request, a skipped one, and two items in one answer.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

The 0.1 ft deduplication tolerance is too large relative to progress, which treats gaps of 0.01 ft or more as missing. Use requestedSpanFt when both plans have server-provided spans and make the threshold strictly less than 0.01 ft, so an extension of exactly 0.01 ft is a new view.

Suggested change
public static let sameViewTolerance: Float = 0.03048
public func asksForSameView(as other: GapPlan) -> Bool {
guard band == other.band, need.asksNoMore(than: other.need, within: Self.reachNoise) else { return false }
// Compare the server's exact endpoints when available: these are the values
// that progress compares against.
if let span = requestedSpanFt, let otherSpan = other.requestedSpanFt {
let tolerance = Self.sameViewToleranceFt
if span.lowerBound <= otherSpan.lowerBound - tolerance ||
span.upperBound >= otherSpan.upperBound + tolerance { return false }
} else {
let tolerance = Self.sameViewTolerance
if span.lowerBound <= other.span.lowerBound - tolerance ||
span.upperBound >= other.span.upperBound + tolerance { return false }
}
if let reach = requestedOutFt, let otherReach = other.requestedOutFt, reach > otherReach {
return false
}
return true
}

Define the tolerances as:

public static let sameViewToleranceFt: Double = 0.01
public static let sameViewTolerance: Float = Float(sameViewToleranceFt) * 0.3048

Add ServerGapTests for span extensions of 0.009, 0.010, and 0.011 ft, each tested after a raised request, after a skipped request, and as the second item in one answer. The expected behavior is:

  • 0.009 ft: deduplicated
  • 0.010 ft: raised as a new request
  • 0.011 ft: raised as a new request

The existing 2.40 → 2.41 ft case is exactly 0.01 ft, so its expectation should change to a newly raised request.

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

@AidenJohnston For your information, the #191 app candidate now carries its own local fix for this, in commit d837c4e. It's unpublished; root publishes after review. Your branch is untouched.

  • Fix: asksForSameView compares the exact requested extents with the same 0.01 ft countsAsMissing test that GapPlanner uses for progress. The 0.1 ft sameViewTolerance is removed.
  • Regressions: an 8-case edge sweep after an asked request, after a skipped one and within one answer, plus aStricterReachIsANewViewAndAnEqualOneIsNot.
  • Evidence so far: the app lane reports 629 package tests passing at d837c4e. The app build and UI tests are still running.

This thread stays open on #49's own head, 6c3ac62. #109 contains the same matcher.

SamGu-NRX added a commit that referenced this pull request Oct 2, 2026
…ence missing (#49)

GapPlan.asksForSameView suppressed a request already asked, skipped or earlier in the same answer
when its span and reach were within 0.1 ft of the other's, measured on the Float-meter span. Progress
measures the server's exact span_ft and counts any uncovered edge of 0.01 ft or more as missing, so a
request for 2...8.09 ft after 2...8 ft disappeared though its last 0.09 ft could never be met; after
a skip the result also marked it not capturable (ScanEngine+Export.swift:198).

Same-view now decides as progress does: the same band and kind of need; a reach no higher than the
other's, compared exactly in requested feet (requestedOutFt, or the need's meters converted when
unset); and no span edge running past the other's by the amount progress counts as missing.
GapPlanner.countsAsMissing is that one comparison, now shared by GapPlanner.fraction and the
same-view rule, and GapPlan.requestedSpanInFeet is the one span both read (requestedSpanFt, or the
span at the export's four decimals when unset). Equal and contained requests are still the same view,
an overhead request without a height still asks no more than one with, and the request limit and
"Show my result" are unchanged. The guessed 0.1 ft sameViewTolerance is gone.

Tests: an 8-case sweep moves one edge outward by 0.009, 0.010, 0.011 and 0.09 ft on each side,
after asked, after skipped and within one answer, with the server's exact span_ft. In doubles
8.01 - 8.0 is just under 0.01 and 2.0 - 1.99 just over it, so 0.010 is rounding on the high edge
and missing on the low edge, for progress and this rule alike; each case is also checked against
GapPlanner.fraction over the old span. A reach test covers stricter, equal and lower reaches and
requests built in meters. Two older expectations encoded the 0.1 ft guess and are changed: the
refused view that came back at 4.9 ft after 4.833334 ft, and a "jitter" item 0.05 ft wider with a
farther reach, both now ask for evidence progress requires. Focused run: ServerGapTests and its
suite, 27 tests passed (job app-pkg-servergap-49).

Co-Authored-By: Claude Code <noreply@anthropic.com>
SamGu-NRX added a commit that referenced this pull request Oct 2, 2026
…ce stays in progress (#49)

Root's fresh review of d837c4e found the 0.01 ft allowance could stack. Coverage seen to exactly
[2, 8] ft meets a request for [2, 8.0085] ft, because progress reads its 0.0085 ft tail as rounding.
The next request, [2, 8.0175] ft, ran only 0.009 ft past that one, so the same-view rule dropped it,
though against the coverage 0.0175 ft is missing and progress is about 0.997.

asksForSameView now requires the span to lie inside the other's, compared exactly in requested feet,
as the reach already was. Rounding tolerance is applied only by progress, against the coverage
actually seen (GapPlanner.countsAsMissing, now used there alone). The server lists only what its own
reading of the uploaded scene still lacks, so an exactly contained request asks for nothing new.
Equal and contained requests stay the same view; the limit and "Show my result" are unchanged.

Tests: aRoundingAllowanceUsedOnceIsNotUsedAgain is root's exact case (old request met with progress
1, next request progress 1 - 0.0175/6.0175, raised after asked and after skipped). The 8-case sweep
now expects every outward edge (0.009, 0.010, 0.011, 0.09 ft, each side) to be a new view after
asked, after skipped and in one answer; anEqualOrContainedRequestIsTheSameView covers equal spans,
edges moved inward by 0.009 ft and a contained span. Focused run: ServerGapTests and its suite,
29 tests passed (job app-pkg-servergap-49b).

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

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:L 100-499 effective changed lines.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants