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:
@@ -146,6 +146,22 @@ public final class BoardStoreRegistry {
|
||||
|
||||
let store = try BoardStore(rootURL: rootURL)
|
||||
|
||||
// **The open-time writability probe** (02-architecture.md § Write-failure surfacing, "An
|
||||
// unwritable board location enters the read-only lock at open"), and this is the seam for
|
||||
// it: every way a board opens — welcome's recents, a Finder open, File ▸ Open, restoration,
|
||||
// a card window arriving first — funnels through `acquire`, so the probe is wired once here
|
||||
// instead of at each caller, and no future open path can forget it.
|
||||
//
|
||||
// **First, immediately after the load.** The lock has to be standing before anything else
|
||||
// in this method can act on the board, and two things below would otherwise write into a
|
||||
// location this line already knows is read-only: the loose-file relocation and the agent
|
||||
// guide. Ordering them after the probe is what makes 02's "the open-time agent-guide write
|
||||
// is skipped-with-log" true by construction rather than by that method's own second gate.
|
||||
//
|
||||
// The board still opens. This is a lock, not a refusal: the window comes up, the snapshot
|
||||
// renders, and reading, selecting, searching and copying out all stay live.
|
||||
store.probeWritabilityAtOpen()
|
||||
|
||||
// Unreachable in practice — the load above just walked this directory — but the alternative
|
||||
// is a force-unwrap on a resource value the filesystem is free to refuse, so it is spelled
|
||||
// out. The loader's own vocabulary says it; no new error path is invented for a case that
|
||||
|
||||
Reference in New Issue
Block a user