@@ -542,17 +542,18 @@ logging and for reasoning about what was lost (in `atomic`, the
542542losing side's bytes are no longer in the resolved file, though they
543543remain reachable in git history).
544544
545- ** Why binary always registers a conflict.** Earlier versions of the
546- plugin used an * atomic mtime* picker for binary files: whichever side
547- had the more recent modification time won, silently. This was
548- correct in the trivial case (one device truly had the newer version)
549- but catastrophic in the non-trivial case (both devices edited the
550- same image independently — the older version was destroyed without
551- the user ever knowing). Pseudo-merge mode rejects the silent picker:
552- binary files always produce a sibling so that the user can see both
553- versions and decide. The user's resolution path is the same as for
554- text — delete the sibling or rename it over the base — but neither
555- version is discarded without their consent.
545+ ** Why binary always registers a conflict.** A plausible alternative
546+ for binary files would be an * atomic mtime* picker — whichever side
547+ has the more recent modification time wins, silently. This is
548+ correct in the trivial case (one device truly has the newer
549+ version) but catastrophic in the non-trivial case (both devices
550+ edited the same image independently — the older version is
551+ destroyed without the user ever knowing). Pseudo-merge mode rejects
552+ the silent picker for that reason: binary files always produce a
553+ sibling so that the user can see both versions and decide. The
554+ user's resolution path is the same as for text — delete the sibling
555+ or rename it over the base — but neither version is discarded
556+ without their consent.
556557
557558** Why plugin bundles use atomic semver.** The bundled ` main.js ` of
558559an Obsidian plugin is minified JavaScript. A three-way diff3 of two
@@ -909,61 +910,55 @@ Step 2; the snapshot in Path A's Step 4), so a crash that destroys
909910the protocol's progress nonetheless leaves enough information on disk
910911for the sweep to decide what was supposed to happen.
911912
912- ** A note on what changed.** Ownership dispatch lives on the ` .sync-tmp `
913- pass, not on ` .sync-bak ` . The reason: forward-direction staging is
914- the operation common to both callsites, and ` .sync-tmp ` is the
915- suffix that denotes that operation regardless of which callsite
916- produced it. An earlier iteration of the design had Path B also
917- produce ` .sync-bak ` files — coercing a forward-target into a
918- suffix that conventionally means "backup of an old version" — and
919- the ownership dispatch lived there instead. That design was
920- semantically misleading: a reader inspecting a ` .sync-bak ` on disk
921- could not know whether it was a backup or a staged new file without
922- checking the conflict store. Aligning each suffix with a single
923- meaning removes that ambiguity from the static reading of the
924- filesystem, and confines the remaining ownership question to
925- ` .sync-tmp ` , where it is also intuitively the right place to ask
926- ("which forward-direction protocol owns this staging file?").
913+ Ownership dispatch lives on the ` .sync-tmp ` pass for a structural
914+ reason: forward-direction staging is the operation common to both
915+ callsites, and ` .sync-tmp ` is the suffix that denotes that
916+ operation regardless of which callsite produced it. The
917+ ownership-versus-orphan question is therefore native to ` .sync-tmp `
918+ and has no analogue on ` .sync-bak ` , which is unambiguous by
919+ construction (only ` atomicWriteFile ` ever produces it). Confining
920+ the dispatch to ` .sync-tmp ` keeps each suffix readable in isolation:
921+ a ` .sync-bak ` on disk always means "backup of a previous file
922+ version," and the question of ownership only arises where the
923+ filesystem itself cannot decide which protocol a staging file
924+ belongs to.
927925
928926### 9.6 Why Vault-Level Staging
929927
930- An earlier iteration of the design used ` <configDir>/.conflicts/<id>/sibling-content.bin `
931- as the staging location for Path B — a private area under the
932- plugin's config directory, parallel to the conflict record itself.
933- Pseudo-merge mode moved staging into the vault, co-located with the
934- eventual sibling, for two reasons:
935-
936- 1 . ** The atomic-write infrastructure already existed.** Path A had
937- been using ` atomicWriteFile ` , with its own recovery sweep that
938- walked the vault for staging files, before pseudo-merge mode
939- landed. Reusing that infrastructure for Path B removed a class of
940- bespoke recovery code and ensures that future fixes to atomic-write
941- semantics propagate to the conflict-store path automatically. The
942- single unified sweep described in §9.5 is the direct dividend.
928+ Path B's staging file lives in the vault, co-located with the
929+ eventual sibling — not in a private area under the plugin's config
930+ directory parallel to the conflict record. Two properties motivate
931+ this placement:
932+
933+ 1 . ** The atomic-write infrastructure is shared with Path A.** Path A
934+ uses ` atomicWriteFile ` with a recovery sweep that walks the vault
935+ for staging files. Routing Path B through the same primitive
936+ removes a class of bespoke recovery code and ensures that any
937+ change to atomic-write semantics propagates to the conflict-store
938+ path automatically. The single unified sweep described in §9.5
939+ is the direct dividend.
9439402 . ** The staging file is recoverable in place.** If the rename in
944941 Path B's Step 3 fails (the OS reports an I/O error, the destination
945942 suddenly exists, etc.), no copying is required to recover — the
946943 bytes are already at the right parent directory, and the recovery
947944 sweep only needs to perform the rename a second time.
948945
949- ### 9.7 What ` load() ` No Longer Does
950-
951- A subtle but important property: the conflict store's ` load() ` does
952- ** not** check the vault for sibling files. It reads ` meta.json `
953- files from the conflict-records directory and builds its in-memory
954- index purely from that. The recovery sweep described in §9.5 runs
955- separately, and the * resolution* of conflicts based on what is or is
956- not in the vault happens only at the next ` drain() ` .
957-
958- This split is deliberate. An earlier design had ` load() ` "restore"
959- missing siblings from a stored backup, on the reasoning that a
960- missing sibling indicated the user had accidentally deleted it. In
961- practice, this design produced a bug: the user * intentionally*
962- deleted a sibling to resolve the conflict, the app quit before the
963- next sync, and on the next plugin load the deleted sibling was
964- "helpfully" restored, undoing the resolution. The current design
965- takes the opposite stance — * filesystem state is authoritative* —
966- and trusts that a missing sibling means the user wanted it gone.
946+ ### 9.7 The Boundary Between ` load() ` and the Recovery Sweep
947+
948+ The conflict store's ` load() ` does ** not** check the vault for
949+ sibling files. It reads ` meta.json ` files from the conflict-records
950+ directory and builds its in-memory index purely from that. The
951+ recovery sweep described in §9.5 runs separately, and the
952+ * resolution* of conflicts based on what is or is not in the vault
953+ happens only at the next ` drain() ` .
954+
955+ This split is deliberate: it makes ** filesystem state authoritative**
956+ for resolution. A missing sibling means the user wanted it gone, and
957+ no part of the load path treats it as something to "restore." The
958+ alternative — having ` load() ` re-emit a missing sibling from a
959+ durable backup — would silently undo the user's intentional deletes
960+ across a plugin restart, which is exactly the failure mode the
961+ "trust the filesystem" rule prevents.
967962
968963---
969964
0 commit comments