Wire the open-time writability probe and read-only lock
Closes the gap found at m10: enterUnwritableLock existed with zero call
sites. WritabilityProbe classifies the cause volume-first - a board on a
read-only DMG is also permission-denied by access(2), and "you don't
have permission" would send the user to a Get Info panel that cannot
help - with a pure classify(volumeIsReadOnly:isWritable:) truth table
and a two-syscall probe that rebuilds its URL to defeat NSURL resource
caching. ReadOnlyLockReason.unwritableLocation now carries the cause;
BannerCenter phrases the two ("this board's volume is read-only" vs
"you don't have permission to change this folder").
The probe wires once in BoardStoreRegistry.acquire, immediately after
the store loads - every open path funnels through it, and running
before the loose-file relocation and agent-guide hooks makes the
skipped-with-log guide write true by construction (its isWritableFile
pre-check demotes to second line of defense). The board still opens:
lock, not refusal.
The reconciling re-probe is now symmetric per 02's settled text - a
volume gone read-only mid-session raises the lock at the next probe
(sibling locks settle first, so a root returning read-only lands the
honest lock); the stale "deliberately one-way" comment and its pinning
test are gone. Save as Template's carve-out predicate extracted to a
testable allowsSave (behavior unchanged); Duplicate stays disabled.
11 tests added. 1649 green on both schemes.
Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY
This commit is contained in:
@@ -165,8 +165,9 @@ public struct InProgressOperation: Identifiable, Sendable {
|
||||
/// and a changing *reason* updates the row rather than replacing it — no view churn, no lost
|
||||
/// animation, and no diffing surprise when a lock's cause changes underneath a standing row.
|
||||
public enum BannerRow: Identifiable, Sendable {
|
||||
/// The board refuses writes. Condition, error tone. Producers: the failed bracketed reload
|
||||
/// (built), the vanished root (this milestone), the open-time writability probe (m4).
|
||||
/// The board refuses writes. Condition, error tone. Producers: the failed bracketed reload, the
|
||||
/// vanished root, and the writability probe — at open and, symmetrically, on every reconciling
|
||||
/// reload thereafter.
|
||||
case readOnlyLock(ReadOnlyLockReason)
|
||||
/// A reload failed and the last good snapshot is still on screen. Condition, error tone.
|
||||
case reloadBreakage(BoardLoadError)
|
||||
@@ -804,14 +805,21 @@ public final class BannerCenter {
|
||||
/// what is on screen is still the last good view — because the lock's whole promise is that
|
||||
/// nothing was lost: reading, selecting, searching and copying out all stay live (02 § "The
|
||||
/// lock's scope").
|
||||
///
|
||||
/// **The unwritable location gets two lines, not one shared one** (02 § Write-failure
|
||||
/// surfacing, settled): "which specific cause, not a shared line … the fixes being different
|
||||
/// acts". Ejecting a DMG or copying the board off it is not the same repair as a `chmod` or a
|
||||
/// Get Info panel, and a line that covered both would name neither.
|
||||
public nonisolated static func headline(for lock: ReadOnlyLockReason) -> String {
|
||||
switch lock {
|
||||
case .bracketedReloadFailed:
|
||||
"This board couldn't be re-read after the last operation — showing the last good view, read-only"
|
||||
case .vanishedRoot:
|
||||
"This board's folder is gone — showing the last good view, read-only"
|
||||
case .unwritableLocation:
|
||||
"This board's location can't be written to — showing the last good view, read-only"
|
||||
case .unwritableLocation(.readOnlyVolume):
|
||||
"This board's volume is read-only — showing the last good view, read-only"
|
||||
case .unwritableLocation(.permissionDenied):
|
||||
"You don't have permission to change this folder — showing the last good view, read-only"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user