Skip to content

ios: candidate: Saved scans shares a kept scan after the app closes (#200 + #202 on #191) - #205

Open
SamGu-NRX wants to merge 18 commits into
codex/public-assembly-20260930from
codex/saved-scans-candidate-20261003
Open

SamGu-NRX wants to merge 18 commits into
codex/public-assembly-20260930from
codex/saved-scans-candidate-20261003

Conversation

@SamGu-NRX

@SamGu-NRX SamGu-NRX commented Oct 3, 2026 •

Copy link
Copy Markdown
Owner

A reviewable candidate, not a ship request. It combines #200 (Saved scans, head 95eee1ed) and #202 (cleanup counts only whole bundles, head 74640d5b) on #191's head 75f6c377, and adds the failed-upload note, a cleanup fix and tests found during integration. #200 and #202 stay open at those heads; nothing here was pushed to their branches.

What changed

After closing the app, a homeowner can still share a finished scan. The first screen's top bar has a Saved scans button. It lists the completed scans the phone keeps (the two most recent). Each row shows when its bundle was saved, its size, and a Practice tag only when the scan's scan-stamp.json says so. Share copies that scan's scan.zip into the app's tmp folder and opens the system share sheet. Nothing leaves the phone until the homeowner picks a destination there. A saved scan can't be reopened or continued, and the sheet says so. It shows no result and restores no AR session, request or placement. Starting a new scan works as before.

A failed upload points to Saved scans. When the scan is packaged, the failed-upload note now reads "Keep this scan open to try again. If you close the app, you can still share its saved file from Saved scans on the first screen." It promises no retry or reopening after close. The note for a scan without a bundle is unchanged. UploadingScreen.swift and its assertion in ScreenStatesUITests.checkOfflineRecovery change together. The backend owner, who is adding an unusable-answer case to the same screen, handed off these lines.

Only bundles written to the end count, are listed or are shared. scan.zip is written in place, so a quit mid-write leaves a partial file under the completed name. The cleanup, the list and Share all use PacketArchiveCheck, which reads at most 64 KiB from each end. A partial, unreadable or linked bundle, or a link in place of a scan folder, takes neither of the two kept places. It isn't listed, and the cleanup doesn't delete it. Share checks the source and the copy again, so a bundle deleted or cut short since listing gives an alert and drops out of the list.

The cleanup deletes only what it listed, still as it was. ScanFolderCleanup lists folders when a new scan's store is made and deletes them later, off the main actor. A folder could gain a bundle in between, or a retry could rewrite one, and it was deleted anyway. run() now reads each listed entry again and leaves any whose type, bundle state, save time or file number changed for the next cleanup. This was added in this candidate, after a review of the combined branch.

A share keeps its own copy. Each app run stages copies under its own session folder in tmp. The sheet deletes its copy when it closes, and launch deletes copies earlier runs left. A new scan's cleanup works on Caches/Scans only, so it can't remove a copy the share sheet is reading.

Why

#130 asks for an app consumer of the completed scans the phone already keeps. Before this, a finished scan survived a relaunch on disk, but only the session that made it could share it.

Producer dependency. The feature reads the producer's existing output and changes no producer code. It relies on these properties of scan.zip as KeyframeStore.zipPacket writes it today:

  • a regular file in a real scan folder;
  • manifest.json as the first entry at offset 0, matching the central directory's first record;
  • a single-disk ZIP32 layout whose end record is written last;
  • the stamp's practice boolean.

If the layout changes, the check fails closed: the scan isn't listed or counted, and its folder is kept. If a producer wrote a valid footer over missing content, or a foreign zip starting with manifest.json were in a scan folder, the check would pass. Its docs now say it detects a write that reached the end, not packet identity. The missing raw-recorder lifetime proof stays an export limitation: Saved scans shares whatever bytes the producer wrote.

UI changes

The failed-upload note, before and after. Both come from hosted CI on the Simulator (iPhone 17) in the frozen -uiDemoOffline state.

Before (b0b785d7, run 37075085816) After (02ff81d7, run 37107646593)
Failed upload note before: Keep this scan open to try again, or use Share scan to save a copy. House Scan can't reopen it after you close the app. Failed upload note after: Keep this scan open to try again. If you close the app, you can still share its saved file from Saved scans on the first screen.

Saved scans is new, so these captures are after-only. They come from hosted run 37075085816, which tested merge commit b0b785d7 (75f6c377 plus #200). Its tree matches this branch's first merge f1767e31, and the sheet hasn't changed since.

Saved scans Share Scan gone since listing Nothing saved yet
Saved scans list with a practice scan tagged Practice and a second scan, each with Share System share sheet offering the dated House Scan zip Alert: This scan is no longer on this phone Empty Saved scans sheet

Verification

Hosted runs check out a merge commit, not the branch head, so each line names the commit that was actually checked out and how its tree compares.

  • ios: Saved scans shares a kept scan after the app is reopened (#130) #200's code on 75f6c377: run 37075085816 checked out b0b785d7 (95eee1ed merged into 75f6c377), whose tree is identical to this branch's f1767e31. HouseScanKit ran 667 tests with warnings as errors, all passing. The UI tests ran 59, with 1 skipped and none failing, including both SavedScansUITests and the full accessibility audit.
  • ios: a partial scan.zip no longer takes a kept place or gets an intact scan deleted #202's code: run 37079907350 checked out b1e3983f, whose tree is identical to 74640d5b. That tree is on the older 0b35f65c base, not 75f6c377. It ran 666 HouseScanKit tests and 48 UI tests, none failing.
  • This branch's combined tree: run 37107646593 checked out 02ff81d7, which merges 041dae0e into 75f6c377. Its tree, 6ece05d7, is identical to this branch's head, so the run tested exactly this branch. HouseScanKit ran 676 tests with warnings as errors, all passing. The UI tests ran 59, with 1 skipped and none failing, including both SavedScansUITests, both offline-recovery tests (standard and largest text size) and the full accessibility audit.

Not run: a real device, a real capture's bundle, and finishing a transfer through AirDrop, Mail or Files. The UI tests open and close the share sheet without sending anything. The app is iPhone-only with UIApplicationSupportsMultipleScenes off, so two windows can't happen today. A HouseScanKit test covers two sheets in one run sharing a staging session.

Risk

  • Abandoned partial bundles stay on the phone. Nothing can tell an abandoned partial from a live write, so they aren't deleted; reclaiming them needs proof that no writer holds them.
  • A change after the cleanup's second read isn't caught, and a rewrite after the check isn't either. Closing those needs the packet writer to replace scan.zip atomically, which is producer code outside this change.
  • If the app is closed during a retry's rewrite, the bundle is partial, so it isn't listed. In that case the failed-upload note's "you can still share" doesn't hold until a whole bundle exists.
  • The archive checks run on the main actor when a scan's store is made: two reads of at most 64 KiB per scan folder. Nobody measured how long they take.

Checklist

  • The scope is focused; related changes belong together
  • I explained what changed and why
  • I included before/after images for visible changes
  • N/A video: the sheet uses system sheet and alert motion and adds no animation of its own

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • Added a Saved scans screen where you can browse and share recent completed scans without reopening them. Scan entries show save time, file size, and an optional Practice label.
    • Added a Saved scans button to onboarding. Sharing remains available for packaged scans after closing the app.
  • Bug Fixes
    • Incomplete or invalid scan files are excluded from the saved-scan list and do not count toward the completed-scan retention limit.
  • Documentation
    • Updated the iOS guide with Saved scans and sharing details.

SamGu-NRX and others added 18 commits October 2, 2026 13:17
…s one for sharing

SavedScanCatalog lists the folders ScanFolderCleanup keeps as completed,
newest first, with each bundle's saved time, size and the stamp's practice
flag when it is a JSON boolean. A bundle counts only when it was written to
the end (PacketArchiveCheck): a quit during the in-place zip write leaves a
partial scan.zip under the completed name. SavedScanStaging copies a bundle
out of the scan folder for the share sheet, so a cleanup can't delete it
mid-share, and removes only its own copies. Links are never scans.

Co-Authored-By: Claude Code <noreply@anthropic.com>
…s reopened (#130)

The onboarding's top bar opens Saved scans: the completed scans the
cleanup keeps, each with its saved time, size and a Practice tag only
when its stamp says so. Share copies the bundle to tmp and hands the copy
to the system share sheet, which deletes it when it closes; a scan gone
since the list was made gets an alert and drops out. Nothing is sent
before Share, and a saved scan is never reopened or continued.

The onboarding holds the sheet's state: the top bar swaps layouts at
accessibility text sizes, and a sheet owned by the button closed when the
text size changed. The empty state scrolls, so its text isn't cut off at
large sizes in the half-height sheet.

Co-Authored-By: Claude Code <noreply@anthropic.com>
…wall wasn't real

Practice mode draws a sample meter, and the walk scans a real wall, so
"not a real wall" was false. The share copy is not assumed to be a free
clone: it can copy every byte and fail on a full disk (copyFailed).

Co-Authored-By: Claude Code <noreply@anthropic.com>
…e, not a system list

Hosted run 37051620836's audit failed the half-height sheet. The
onboarding showed through its glass behind the intro and footer
(contrast), the list's header and footer and the navigation bar's Done
didn't fully follow Dynamic Type, and large text ran off the bottom. The
sheet is now a scrolling column on the canvas, with the rows on a surface
card and the title and Done in the app's fonts, like the onboarding.

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

A share sheet open when the app is killed never reports back, so its copy
stayed in tmp until Saved scans was opened again. Launch now removes the
copies made before it; a share started right after launch keeps its own.

Co-Authored-By: Claude Code <noreply@anthropic.com>
…, in the app's init

A window's .task runs again for each window or reappearance, and a later
cutoff could cover a copy this run is still sharing.

Co-Authored-By: Claude Code <noreply@anthropic.com>
…t scan deleted

ScanFolderCleanup counted any scan.zip as a completed scan, so a bundle
cut short by a quit during its in-place write held one of the two kept
places and an intact older scan was deleted instead. Completed now means
PacketArchiveCheck finds a whole packet, still the same file after the
check. A partial or unreadable bundle holds no place and its folder is
left alone at any age: nothing proves its writer has stopped. Retention
count, order, listing at construction and the current-folder guard are
unchanged.

Co-Authored-By: Claude Code <noreply@anthropic.com>
… not just the file

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

The file-number and size comparison had no test that swaps the file
mid-check, so it could stop catching a retry's rewrite unnoticed. The
check is injectable within the module for that test.

Co-Authored-By: Claude Code <noreply@anthropic.com>
…es the others

A launch-time cutoff depended on the clock: set back between runs, an
earlier run's copy looked newer and stayed. Each run now stages under a
session folder of its own, and launch, or opening Saved scans, deletes the
other sessions' folders, so no copy a share in progress is reading can
match. removeAll is gone with its last caller.

Co-Authored-By: Claude Code <noreply@anthropic.com>
…lace holds no kept place

A top-level link to a scan folder resolved to that scan's bundle, so the
link and its target could take both kept places and an intact scan was
deleted. A link, or an entry whose type can't be read, now holds no place
and is left alone; a plain file still goes, as before.

Co-Authored-By: Claude Code <noreply@anthropic.com>
…ss a new scan's cleanup

Two sheets in one process share a staging session, so opening one never
deletes the copy the other is sharing. A copy being shared keeps the
bundle's bytes after a new scan's cleanup deletes its source folder.

Co-Authored-By: Claude Code <noreply@anthropic.com>
…d file after the app closes

With the scan packaged, the note now says that closing the app ends the
retry but not the sharing: Saved scans on the first screen offers the
same bundle. It promises no reopening or retry after close. The note
without a bundle is unchanged. The test assertion moves with it.
Written with the backend owner's handoff of these lines.

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

The deletion runs after the listing, off the main actor. A folder listed
as never packaged could gain a bundle, partial or whole, before it ran,
and was deleted anyway; so was a kept-out bundle a retry had rewritten.
run() now reads each obsolete entry again and leaves any that changed,
by type, bundle state, save time or file number, for the next cleanup.
PacketArchiveCheck's docs now say it detects a write that reached the
end in the packet's layout, not that a file is a capture packet.

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

Co-Authored-By: Claude Code <noreply@anthropic.com>
@SamGu-NRX SamGu-NRX added the full-ui iOS CI also runs the every-state accessibility audit (about 10 more minutes). label Oct 3, 2026
@codemetrics-ai

codemetrics-ai Bot commented Oct 3, 2026

Copy link
Copy Markdown

Note

Review skipped

Codemetrics is installed on this repository, but it hasn't been added to your team yet.

To enable AI code reviews for this repo, go to your dashboard and add BaseScanning to your team's repositories.

@github-actions github-actions Bot added the size:XL 500-999 effective changed lines. label Oct 3, 2026
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 3, 2026 •

Copy link
Copy Markdown

Codex Review Summary

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

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-03T07:55:29.859313Z 041dae0 PR opened
ℹ️ 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.

@coderabbitai

coderabbitai Bot commented Oct 3, 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: 4eb75703-1be5-4f9e-9aa9-c4cadf4042cd
📥 Commits

Reviewing files that changed from the base of the PR and between 75f6c37 and 041dae0.

📒 Files selected for processing (19)
  • ios/HouseScan/HouseScanApp.swift
  • ios/HouseScan/Runtime/KeyframeStore.swift
  • ios/HouseScan/UI/SavedScans/ActivitySheet.swift
  • ios/HouseScan/UI/SavedScans/SavedScanRow.swift
  • ios/HouseScan/UI/SavedScans/SavedScansButton.swift
  • ios/HouseScan/UI/SavedScans/SavedScansCopy.swift
  • ios/HouseScan/UI/SavedScans/SavedScansLocation.swift
  • ios/HouseScan/UI/SavedScans/SavedScansSheet.swift
  • ios/HouseScan/UI/Screens/OnboardingScreen.swift
  • ios/HouseScan/UI/Screens/UploadingScreen.swift
  • ios/HouseScanKit/Sources/HouseScanKit/SavedScans/PacketArchiveCheck.swift
  • ios/HouseScanKit/Sources/HouseScanKit/SavedScans/SavedScan.swift
  • ios/HouseScanKit/Sources/HouseScanKit/SavedScans/SavedScanCatalog.swift
  • ios/HouseScanKit/Sources/HouseScanKit/Scene/ScanFolderCleanup.swift
  • ios/HouseScanKit/Tests/HouseScanKitTests/KeptScansTests.swift
  • ios/HouseScanKit/Tests/HouseScanKitTests/SavedScanCatalogTests.swift
  • ios/HouseScanUITests/SavedScansUITests.swift
  • ios/HouseScanUITests/ScreenStatesUITests.swift
  • ios/README.md

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 change adds a saved-scan catalog and sharing interface. It verifies scan archives before listing or cleanup, stages verified copies for sharing, and removes staged copies after sharing or during later-session cleanup.

Changes

Saved scans

Layer / File(s) Summary
Verify archives and classify cleanup candidates
ios/HouseScanKit/Sources/HouseScanKit/SavedScans/PacketArchiveCheck.swift, ios/HouseScanKit/Sources/HouseScanKit/Scene/ScanFolderCleanup.swift, ios/HouseScanKit/Tests/HouseScanKitTests/KeptScansTests.swift
Archive checks validate packet structure. Cleanup counts verified complete bundles toward the retention limit, leaves incomplete or unjudged entries alone, and rechecks obsolete entries before deleting them. Tests cover these classifications and deletion cases.
Catalog saved scans and stage share copies
ios/HouseScanKit/Sources/HouseScanKit/SavedScans/SavedScan.swift, ios/HouseScanKit/Sources/HouseScanKit/SavedScans/SavedScanCatalog.swift, ios/HouseScanKit/Tests/HouseScanKitTests/SavedScanCatalogTests.swift
The catalog returns verified completed scans with archive metadata and optional practice status. Staging verifies and copies archives into session-specific folders. Tests cover listing, staging errors, copy removal, and share filenames.
Connect Saved scans to the app and share flow
ios/HouseScan/Runtime/KeyframeStore.swift, ios/HouseScan/HouseScanApp.swift, ios/HouseScan/UI/SavedScans/*, ios/HouseScan/UI/Screens/OnboardingScreen.swift, ios/HouseScan/UI/Screens/UploadingScreen.swift, ios/HouseScanUITests/SavedScansUITests.swift, ios/HouseScanUITests/ScreenStatesUITests.swift, ios/README.md
The app configures catalog and staging locations, exposes Saved scans in onboarding, and shares staged files through the system activity controller. The sheet removes a staged copy after dismissal, and app startup removes copies from other sessions. UI tests and the README describe and validate this flow.

Priority: ➖ Normal

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

Change: Feature

Sequence Diagram(s)

sequenceDiagram
  actor User
  participant OnboardingScreen
  participant SavedScansSheet
  participant SavedScanCatalog
  participant SavedScanStaging
  participant ActivitySheet
  participant UIActivityViewController
  User->>OnboardingScreen: Open Saved scans
  OnboardingScreen->>SavedScansSheet: Present sheet
  SavedScansSheet->>SavedScanCatalog: Load saved scans
  SavedScanCatalog-->>SavedScansSheet: Return scan list
  User->>SavedScansSheet: Select Share
  SavedScansSheet->>SavedScanStaging: Stage verified archive copy
  SavedScanStaging-->>SavedScansSheet: Return staged file URL
  SavedScansSheet->>ActivitySheet: Present staged file
  ActivitySheet->>UIActivityViewController: Present share interface
  UIActivityViewController-->>ActivitySheet: Complete or cancel activity
  ActivitySheet-->>SavedScansSheet: Run completion closure
  SavedScansSheet->>SavedScanStaging: Remove staged copy
Loading

Merge Risk: ⚪ Minimal · up to 041da

No actionable issue is established for the Saved scans change. Complete the normal combined-tree and device checks before shipping.

Security Architecture Review

Security architecture risk: 🔵 Low · up to 041da

Sharing remains user-directed, and temporary copies have cleanup paths. The remaining design concern is that interrupted scans can retain private capture data without an app-enforced expiry, outside the two-completed-scan limit.

Retained concerns

  • Low · security · observed: Incomplete and unreadable scan folders now have no app-enforced retention limit or reclamation transition. They can retain private capture data across subsequent scans while remaining invisible in Saved scans. The base counted readable scan.zip files toward bounded retention. Preserving potentially active writers prevents unsafe deletion, but also preserves abandoned partial bundles after process interruption; eventual operating-system cache purging is not an app-owned terminal state.
Security review details

Security Blast Radius

  • inferred — The evidenced retention exposure is private capture data within one installation's local scan cache. The export path transfers one selected archive through the system activity controller. No cross-tenant, backend credential or privileged service transition is established by this inspected flow.

Security Findings and Attack Paths

  • inferred — Interrupted packaging can leave a partial bundle that subsequent cleanup preserves indefinitely and the catalog omits. Repeated interrupted scans can therefore accumulate private local data beyond the completed-scan limit. This requires local capture and interruption; no remote trigger or unauthorized export path was established.

Trust Boundaries and Controls

  • observed — Opening Saved scans reads the catalog rather than transmitting data or restoring a scan session. Export requires a separate Share action and presents the system destination chooser. Independent source and copy verification controls prevent structurally incomplete copies from reaching that chooser.

Resilience and Maintainability Implications

  • observed — The UI serializes preparation and blocks user dismissal during copying. Completion, cancellation and swipe dismissal converge on staged-copy cleanup. Restart cleanup covers abandoned prior-session copies, although filesystem removal is best-effort rather than confirmed deletion.

Hardening Proposals

  • proposed — Introduce explicit writer ownership and a recovery transition that can prove an incomplete folder has no active writer before reclaiming it. Apply retention bounds to those abandoned artifacts without using file age alone to delete potentially live captures.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 62.34% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 77 functions across 18 files. (1 skipped:… Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title identifies the main change: sharing saved scans after the app closes. The candidate label and issue references add some noise but do not obscure the purpose.
Description check ✅ Passed The description covers what changed, why, UI changes, verification, risks, and the checklist. It includes relevant screenshots and states which tests were not run.
Full details: Docstring Coverage

Explanation

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

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

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.

@greptile-apps

greptile-apps Bot commented Oct 3, 2026 •

Copy link
Copy Markdown

RetriggerConfidence Score: 4/5

[Medium risk] Adds saved scan sharing feature to the app.

The PR is not yet safe to merge because deferred cleanup can still delete a folder during an in-flight bundle write.

Fix All in CodexFindings

  1. P1 Cleanup can delete an active write ▶
  2. P2 Partial bundles accumulate indefinitely ▶
Fix with agent prompt
### Issue 1
ios/HouseScanKit/Sources/HouseScanKit/Scene/ScanFolderCleanup.swift:140
When a scan is started over, its packet write can continue while the new store runs cleanup. If that write starts after this check but before `removeItem`, cleanup deletes the folder the writer is using. The write can fail and the scan can be lost. The recheck narrows the window, but deletion and writing still need coordination.

### Issue 2
ios/HouseScanKit/Sources/HouseScanKit/Scene/ScanFolderCleanup.swift:60-69
Every partial or unreadable bundle is kept regardless of age or count. That protects a live write, but interrupted writes left by earlier app runs have no reclamation path. Over time, those abandoned bundles can use the storage needed to finish or share another scan.

---

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

This PR adds a Saved scans sheet for sharing completed bundles after relaunch, validates bundles before listing and sharing, and rechecks folders before deferred cleanup.

  • Stages each share outside the scan folders and removes its copy when the share sheet closes.
  • Updates failed-upload guidance and adds catalog, cleanup, and UI coverage.
  • Deferred deletion still has a check-to-delete race, and abandoned partial bundles have no reclamation path.
Diagram
%%{init: {'theme': 'neutral'}}%%
flowchart LR
  A["Scan folders"] --> B["Archive verification"]
  B --> C["Two newest completed scans"]
  C --> D["Saved scans"]
  D --> E["Copy to temporary staging"]
  E --> F["System share sheet"]
  F --> G["Remove staged copy"]
  A --> H["Deferred cleanup"]
  H --> I["Recheck listed folders"]
  I --> J["Delete obsolete folders"]
Loading

Reviews (1) · Last reviewed commit: "ios: the README says which scans Saved s..."

let files = FileManager.default
return obsolete.compactMap { url in
guard files.fileExists(atPath: url.path) else { return nil }
guard let listed = listedAs[url], Self.entry(at: url) == listed else { return nil }

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 Cleanup can delete an active write

When a scan is started over, its packet write can continue while the new store runs cleanup. If that write starts after this check but before removeItem, cleanup deletes the folder the writer is using. The write can fail and the scan can be lost. The recheck narrows the window, but deletion and writing still need coordination.

Prompt To Fix With AI
This is a comment left during a code review.
Path: ios/HouseScanKit/Sources/HouseScanKit/Scene/ScanFolderCleanup.swift
Line: 140

Comment:
**Cleanup can delete an active write**

When a scan is started over, its packet write can continue while the new store runs cleanup. If that write starts after this check but before `removeItem`, cleanup deletes the folder the writer is using. The write can fail and the scan can be lost. The recheck narrows the window, but deletion and writing still need coordination.

---

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

Fix in Codex Fix in Claude Code

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Yes. That window is real, and this change doesn't close it. run() reads each listed entry again right before deleting it, so a write that started any time before that read keeps the folder (lines 132–135). A write whose first byte lands between that read and removeItem is still lost. Nothing on the reading side can see it: removing the folder still races with a writer that hasn't started yet.

Closing it needs the writer to cooperate. Either the packet code registers the folder it's writing to, so the cleanup can skip it, or it writes scan.zip under another name and renames it into place. Both are producer changes, and this candidate deliberately touches no producer code. The PR's Risk section and the comments at lines 107–109 and 132–135 state this limit.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Agreed. The second read protects against changes already observable when cleanup rechecks the folder, but it cannot detect a writer that starts in the final read-to-removeItem interval. Closing that last race requires producer-side coordination—such as writer registration—or atomic publication by writing elsewhere and renaming, both outside this candidate’s scope.

With the limitation explicitly documented in the PR, I consider this a known residual risk rather than an additional issue with this change.

Comment on lines +60 to +69
case .unjudged, .folder(.incomplete):
incomplete.append(name)
}
}
completed.sort { ($0.date, $0.name) > ($1.date, $1.name) }
let keptNames = completed.prefix(max(0, keepCompleted)).map(\.name)
let keep = Set(keptNames + incomplete)
keptCompleted = keptNames.map { root.appending(path: $0) }
incompleteBundles = incomplete.map { root.appending(path: $0) }
let obsoleteNames = names.filter { !keep.contains($0) }

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 Partial bundles accumulate indefinitely

Every partial or unreadable bundle is kept regardless of age or count. That protects a live write, but interrupted writes left by earlier app runs have no reclamation path. Over time, those abandoned bundles can use the storage needed to finish or share another scan.

Prompt To Fix With AI
This is a comment left during a code review.
Path: ios/HouseScanKit/Sources/HouseScanKit/Scene/ScanFolderCleanup.swift
Line: 60-69

Comment:
**Partial bundles accumulate indefinitely**

Every partial or unreadable bundle is kept regardless of age or count. That protects a live write, but interrupted writes left by earlier app runs have no reclamation path. Over time, those abandoned bundles can use the storage needed to finish or share another scan.

---

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

Fix in Codex Fix in Claude Code

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

This is the trade-off we chose on purpose, and the type's doc comment (lines 17–23) and the PR's Risk section both state it. A partial bundle can't be told apart from one whose writer is paused: Start over can leave the previous scan's write running, and a suspended phone can pause a write for any length of time. So deleting by age or count could take a folder out from under a live write and lose that scan. An age-based grace period was considered and rejected, because age doesn't prove the writer finished. Reclaiming abandoned partials safely needs proof that no writer holds them, and that belongs in a separate change together with the producer.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

That makes sense. Given the deliberate safety trade-off and the documented limitation, I agree this is not an actionable issue for this change. Safe reclamation should be handled separately alongside producer-side writer-lifetime or atomicity guarantees.

@SamGu-NRX

Copy link
Copy Markdown
Owner Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Oct 3, 2026 •

Copy link
Copy Markdown
✅ Action performed

Review finished.

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

SamGu-NRX added a commit that referenced this pull request Oct 3, 2026
… packaged

#203 wrote its notes before #205's Saved scans existed, so a packaged scan's note
still said House Scan can't reopen it after the app closes, with no mention that
Saved scans on the first screen can share its file then. Now the first note says
so and that it can't try again from there, and the repeated note adds that Saved
scans can share it later. Without a packaged scan the notes are unchanged, as is
the failed-upload note #205 already aligned.

Co-Authored-By: Claude Opus 5.5 (1M context) <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

full-ui iOS CI also runs the every-state accessibility audit (about 10 more minutes). size:XL 500-999 effective changed lines.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant