Skip to content

Commit 6997ea7

Browse files
dbdeveloperclaude
andcommitted
docs: rewrite retrospective passages in present-tense canonical voice
The pseudo-merge article is an official design document, not a changelog. Three places framed the design comparatively against prior iterations ("earlier versions used X", "an earlier design had Y") — rewritten to state the design as-is: - §7 binary classifier: "Earlier versions used atomic mtime" → "A plausible alternative would be atomic mtime" (framed as a rejected design option, not a historical artefact). - §9.5 closing paragraph: removed the "A note on what changed" retrospective block; replaced with a present-tense statement of why ownership dispatch lives on .sync-tmp. - §9.6 opening: dropped "An earlier iteration of the design used <configDir>/.conflicts/<id>/sibling-content.bin"; reframed as a positive statement of why vault-level staging is the design. - §9.7 heading: "What `load()` No Longer Does" → "The Boundary Between `load()` and the Recovery Sweep"; body reframed in present tense (filesystem state is authoritative, full stop). No technical content changed; only the framing. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 084dacf commit 6997ea7

1 file changed

Lines changed: 52 additions & 57 deletions

File tree

docs/PSEUDO-MERGE-MODE.md

Lines changed: 52 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -542,17 +542,18 @@ logging and for reasoning about what was lost (in `atomic`, the
542542
losing side's bytes are no longer in the resolved file, though they
543543
remain 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
558559
an 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
909910
the protocol's progress nonetheless leaves enough information on disk
910911
for 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.
943940
2. **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

Comments
 (0)