M1-12: hand the documents directory to the user #37

Merged
wk merged 1 commit from m1-12-file-sharing into main 2026-08-08 20:00:37 -04:00
Member

Closes #12.

UIFileSharingEnabled and LSSupportsOpeningDocumentsInPlace put every memo in Finder over a
cable and under On My iPhone in the Files app. The recordings already live in the documents
directory and the format is plain files, so nothing moved and nothing is exported.
UISupportsDocumentBrowser is deliberately not set: it lets the save location be re-pointed from
Settings, which would move memos out from under the one directory the list reads.

What the keys cost, and what it took

The folder is no longer only what the app put there, and two paths used to remove a capture's
.parts folder recursively. They now share one rule — TranscriptAssembly.clearParts, used by
finalisation and by swipe-to-delete:

  • Only a transcript part of this capture, the <the part>.sb-<hex>-<random> temporary an
    interrupted atomic write leaves, .DS_Store, and a ._ sidecar for a part are removed. A
    sidecar for somebody's document is part of their document.
  • Removal is unlink, the folder goes by rmdir, and neither can take a directory — so there is
    no window between deciding what a path is and removing it. Same for the audio, the transcript,
    and the .assembling temporary.
  • A folder that survives is a deletion failure, which keeps the audio and so keeps the row: the
    memo is still deletable once the file has been moved out.

clearParts lists by path, not by URL. The URL enumerator does not return ._ entries —
measured — so a sidecar could otherwise have made a memo permanently undeletable from its row
while rmdir still counted it.

The launch sweep reads every part of every unfinished capture before anything is on screen, so a
part over 4 MB, or one taking the capture past 16 MB in total, is counted missing rather than
read. Recovery also now requires a regular file at the transcript's name; a directory there used
to read as "already finished" and skip the parts beside it silently, at every launch.

Reviews

Four Codex passes in .agent/m1-12-file-sharing/, with every finding's disposition in
SUMMARY.md. The one rejected three times is coordinating file access or staging in-flight
artifacts outside Documents: staging puts a finished memo where the list does not read, which is a
second path through the thing this format keeps simple, and coordination buys nothing against
Finder, which does not coordinate. The reasoning, including what was rejected, is in stash-docs.

Validated

  • 142 unit tests, make lint clean, builds and launches in the simulator.
  • Both keys and the absence of UISupportsDocumentBrowser are asserted against the built
    Info.plist, read through Bundle.main.
  • Every new rule has a test that fails if the rule is reverted.

Not validated

  • Nothing on the device. Finder, the Files app, and dragging a memo off are issue #12's
    acceptance and have not been run — the needs-hardware pass is still owed.
  • Whether iOS lists ._ entries the way the simulator does.
  • chflags and chmod fixtures exercise the failure paths, not the device conditions that would
    produce them.

Separately

The decision file for this is written but not committed — it is the first one in stash-docs,
which was empty, so it invents decisions/YYYY-MM-DD-*.md and a README line. Worth a look before
it goes anywhere.

Closes #12. `UIFileSharingEnabled` and `LSSupportsOpeningDocumentsInPlace` put every memo in Finder over a cable and under On My iPhone in the Files app. The recordings already live in the documents directory and the format is plain files, so nothing moved and nothing is exported. `UISupportsDocumentBrowser` is deliberately not set: it lets the save location be re-pointed from Settings, which would move memos out from under the one directory the list reads. ## What the keys cost, and what it took The folder is no longer only what the app put there, and two paths used to remove a capture's `.parts` folder recursively. They now share one rule — `TranscriptAssembly.clearParts`, used by finalisation and by swipe-to-delete: - Only a transcript part of this capture, the `<the part>.sb-<hex>-<random>` temporary an interrupted atomic write leaves, `.DS_Store`, and a `._` sidecar **for a part** are removed. A sidecar for somebody's document is part of their document. - Removal is `unlink`, the folder goes by `rmdir`, and neither can take a directory — so there is no window between deciding what a path is and removing it. Same for the audio, the transcript, and the `.assembling` temporary. - A folder that survives is a deletion failure, which keeps the audio and so keeps the row: the memo is still deletable once the file has been moved out. `clearParts` lists by path, not by URL. The URL enumerator does not return `._` entries — measured — so a sidecar could otherwise have made a memo permanently undeletable from its row while `rmdir` still counted it. The launch sweep reads every part of every unfinished capture before anything is on screen, so a part over 4 MB, or one taking the capture past 16 MB in total, is counted missing rather than read. Recovery also now requires a *regular file* at the transcript's name; a directory there used to read as "already finished" and skip the parts beside it silently, at every launch. ## Reviews Four Codex passes in `.agent/m1-12-file-sharing/`, with every finding's disposition in `SUMMARY.md`. The one rejected three times is coordinating file access or staging in-flight artifacts outside Documents: staging puts a finished memo where the list does not read, which is a second path through the thing this format keeps simple, and coordination buys nothing against Finder, which does not coordinate. The reasoning, including what was rejected, is in `stash-docs`. ## Validated - 142 unit tests, `make lint` clean, builds and launches in the simulator. - Both keys and the absence of `UISupportsDocumentBrowser` are asserted against the **built** `Info.plist`, read through `Bundle.main`. - Every new rule has a test that fails if the rule is reverted. ## Not validated - **Nothing on the device.** Finder, the Files app, and dragging a memo off are issue #12's acceptance and have not been run — the `needs-hardware` pass is still owed. - Whether iOS lists `._` entries the way the simulator does. - `chflags` and `chmod` fixtures exercise the failure paths, not the device conditions that would produce them. ## Separately The decision file for this is written but **not committed** — it is the first one in `stash-docs`, which was empty, so it invents `decisions/YYYY-MM-DD-*.md` and a README line. Worth a look before it goes anywhere.
`UIFileSharingEnabled` and `LSSupportsOpeningDocumentsInPlace` put every memo
in Finder over a cable and under On My iPhone in the Files app. The recordings
already live there, so nothing moved.

What the two keys cost is that the folder is no longer only what the app put
there. Finalisation and swipe-to-delete now share one rule for clearing a
capture's parts folder: only a part of this capture, the temporary an
interrupted atomic write left, `.DS_Store`, and a sidecar for a part are
removed, with `unlink` and `rmdir` so neither can take a directory. A folder
holding anything else stays, and deleting a memo says so and keeps the row.

The launch sweep is bounded too — a part over four megabytes, or one that would
take the capture past sixteen, is counted missing rather than read, so a file
dropped into a capture-shaped folder cannot stop the app opening.

Not confirmed on the device: whether Finder and the Files app show the folder,
which is issue 12's acceptance, and whether iOS lists `._` entries the way the
simulator does.
wk merged commit 2667aed76e into main 2026-08-08 20:00:37 -04:00
wk deleted branch m1-12-file-sharing 2026-08-08 20:00:40 -04:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
Stash/stash-ios!37
No description provided.