mirror of
https://github.com/PR0M3TH3AN/SeedPass.git
synced 2026-10-31 01:42:05 +00:00
The deterministic merge is frozen for parity -- both implementations must
reach the same vault from the same inputs -- so how it resolves is not up for
negotiation. What was missing is that nobody is told a resolution happened.
L-2: the allocation watermark is per-replica, so two devices working offline
can both allocate id N. The merge then resolves that id by timestamp then
hash, and the loser is replaced with no trace. An entry the user created is
simply absent afterwards. Worse than a plain overwrite, because an id is a
permanent BIP-85 derivation coordinate: the survivor derives from the same
coordinate the discarded entry did (see the L-1 detector).
L-3: past TOMBSTONE_RETENTION_CAP the oldest deletions are forgotten, and
merging a stale replica or old snapshot then resurrects those entries. A
documented protocol limit -- but a user cannot know the cap has actually begun
trimming their history unless something says so.
Both implementations gain an optional MergeReport collector: same-id conflicts
(with the kept and discarded entries named) and a count of evicted tombstones.
`nostr restore` reports both in its summary and on stderr, covering delta
replay as well as the final local merge.
Only genuine divergences are reported. Two replicas holding the same entry
with different timestamps is an ordinary edit and is ignored; flagging it
would bury the case that matters in noise and train the user past the warning.
The collector is write-only and cannot change the merged result -- asserted in
both implementations, because if observing could alter the outcome it would be
a parity divergence rather than an observation.
Two defects found while writing the tests:
- Python's merge MUTATED its `current` argument. `dict(current)` shares the
nested entries dict, so the merge wrote its result straight into the
caller's payload: after merging, "current" had already become the merged
value. Anything comparing before-and-after, or merging one payload twice,
silently saw the wrong input. Now deep-copied, matching TypeScript's
structuredClone. Found because the "reported whichever side wins" test
failed -- the forward merge had corrupted the input for the backward one.
- test_merge_replay_same_payload_is_idempotent was passing only because of
that aliasing: `once` and `twice` were one object. With inputs no longer
mutated, replaying the same payload is shown to normalize entry SHAPE --
the equal-timestamp path unions field sets and writes explicit nulls for
absent optional fields. TypeScript produces byte-identical output for the
same input, so this is shared designed behaviour rather than a Python
regression, and it converges on the second pass. The test now asserts what
is actually true (content unchanged, shape normalized, settles at two) and
records why the old assertion held.
Mutation-verified: removing the recording turns the reporting tests red.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01Vtx94nXDmNgXEeThUZP4QN