NEC-276: NEC-268 stage A: a provenance ledger per file, and content-sync onto it #306

Merged
nectenda-agent merged 9 commits from worktree-nec-276-nec-268-stage-a-a-provenance-ledger-per into main 2026-10-19 01:48:56 +01:00
Collaborator

Task: NEC-276 — https://projectron.nerchure.com/tasks/287

Stage A of NEC-268: content-sync moves onto one provenance ledger per note (provenance-ledger.ts, with a synchronous MurmurHash3 x86_128 text hash), replacing the record, the session's contained texts, the bound editor's in-memory saves and the own-write flag. writeToDisk reads the file once; its re-reads after awaits are generation checks, which also catch A-B-A. Bound saves persist across a quit, and the editor's let-go text is one of them, so its late save after our write no longer removes a remote change. Migration from the v1 record with a tombstone, downgrade detection, rename carry, and the structured takeover reading the note ledger. Decisions 14-18 in the spec came from the extended property test and a fresh-context review. Rides along: NEC-307 (a stale second reader taking a file in twice) and NEC-308 (an unknown file written over with no copy after a first write that did not land). Local gate green; Markdown hardness 63 min over two seeds (one failure reproduced identically on main, filed as NEC-309).

Spec: docs/changes/NEC-276-provenance-ledger-stage-a/spec.md

Also fixes NEC-307: Two content-sync readers merge after an await without checking the file was not taken in meanwhile
Also fixes NEC-308: A note with nothing recorded is written over with no copy after a first write that did not land

Changelog

Notes keep track of what an open editor saved, across a restart too, so a save that lands late no longer brings back deleted text or removes a collaborator's change.

Task: NEC-276 — https://projectron.nerchure.com/tasks/287 Stage A of NEC-268: content-sync moves onto one provenance ledger per note (provenance-ledger.ts, with a synchronous MurmurHash3 x86_128 text hash), replacing the record, the session's contained texts, the bound editor's in-memory saves and the own-write flag. writeToDisk reads the file once; its re-reads after awaits are generation checks, which also catch A-B-A. Bound saves persist across a quit, and the editor's let-go text is one of them, so its late save after our write no longer removes a remote change. Migration from the v1 record with a tombstone, downgrade detection, rename carry, and the structured takeover reading the note ledger. Decisions 14-18 in the spec came from the extended property test and a fresh-context review. Rides along: NEC-307 (a stale second reader taking a file in twice) and NEC-308 (an unknown file written over with no copy after a first write that did not land). Local gate green; Markdown hardness 63 min over two seeds (one failure reproduced identically on main, filed as NEC-309). Spec: `docs/changes/NEC-276-provenance-ledger-stage-a/spec.md` Also fixes NEC-307: Two content-sync readers merge after an await without checking the file was not taken in meanwhile Also fixes NEC-308: A note with nothing recorded is written over with no copy after a first write that did not land ## Changelog Notes keep track of what an open editor saved, across a restart too, so a save that lands late no longer brings back deleted text or removes a collaborator's change.
One ledger per file: the agreed text, the last disk text accounted for, a
write in progress, and hashed entries for texts the document is known to
contain, persisted as one write. Classification is synchronous and pure.
Migration from the v1 record, a tombstone for the old key, and a record a
downgraded plugin wrote after the ledger migrated from again.

Nothing uses it yet: content-sync moves onto it next.

Task: NEC-276

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J4oTDLYck3yQMFPJrLAA6j
The record, the session's contained texts, the bound editor's saves and
the own-write flag become one ledger. Bound saves are stored, so one
landing after a quit is still the editor's; the text an editor held as it
let go is one of its saves, so its late save after our write of a remote
change no longer removes that change. Echoes are recognised by what the
file says. writeToDisk reads the file once: its re-reads after awaits are
generation checks, which also catch a file put back to what was read.
The structured takeover reads the note ledger.

Task: NEC-276

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J4oTDLYck3yQMFPJrLAA6j
Behind, read and our-write texts are what this session saw the file pass
through; the file record never stored them. Stored, the next session read
a user's edit putting the file back to one of them as behind and undid it:
over 4000 sequences of the in-process randomised test with the original
actions, ten or more such deletes came back, against three on main. Kept
in memory only, five; the two beyond main's are a file put back to exactly
what an editor held as it let go, moments after our write, which is the
case a let-go save landing late must be read as.

The load properties now say what holds: classification after a load is
the stored part's, and never stronger than before it.

Task: NEC-276

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J4oTDLYck3yQMFPJrLAA6j
reconcileFromDisk merged after keeping its copy, and onLocalModify's
unknown path read in after keeping the document, each on a decision made
before that await. Another reader of the same file - the connect's first
write as a restored tab binds, or a second modify event - could take the
file in meanwhile, and the document move on past it: merged again, a line
deleted since came back. Both now drop a decision another reader has
overtaken.

Task: NEC-276
Also-fixes: NEC-307

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J4oTDLYck3yQMFPJrLAA6j
SAFE-A35 now describes a note's ledger beside a structured file's record:
bound saves kept across a quit and our own write, texts kept whole only
where they can be a merge base, an unknown text never behind, forgetting
only ever weakening, and generation checks in place of re-reads. SAFE-C1's
baseline names the ledger. sync-limitations records the six faults closed,
and the cost measured: a put-back to an editor's let-go text, moments after
our write, is undone.

Task: NEC-276

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J4oTDLYck3yQMFPJrLAA6j
From the review. A bound save stored past our own write came back live
at the next start, however long ago it had expired in memory: the store
holds what the last put saw, and an expiry puts nothing. A file put back
to it while Obsidian was closed was written over; main honours it. Such
a save now stays in memory for its moments; the one a quit can strand,
which no agreement has passed, is still stored.

A store migrated from the legacy agreed text alone gets its record key
tombstoned too, and an echo must be a write this session began.

L7 now holds the put, L3 makes the put fail and stops the note during
it, L9 opens a view, and L13 checks the disk text crosses the rename:
each failed under its rule's removal.

Task: NEC-276

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J4oTDLYck3yQMFPJrLAA6j
A connect's first write answers the first-sync question before it writes.
Refused or abandoned, it left the note with nothing recorded and the
question answered, and no branch of writeToDisk kept the file: an edit
met then was written over with no copy while its own modify event was
still keeping the document, and was in no file, no document and no
backup. The file is now kept first.

Task: NEC-276
Also-fixes: NEC-308

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J4oTDLYck3yQMFPJrLAA6j
Record a board view's stale save against another writer's delete
Some checks failed
CI / promote (pull_request) Has been cancelled
CI / build (pull_request) Has been cancelled
CI / e2e (pull_request) Has been cancelled
Release note / release-note (pull_request) Successful in 20s
f0686bdaba
Found by the Markdown hardness run (seed 281144514, round 159) and the same on main: a card deleted by another writer while a board view has an edit in progress comes back when the view saves. Filed as NEC-309.

Task: NEC-276

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J4oTDLYck3yQMFPJrLAA6j
cruelacid referenced this pull request from a commit 2026-10-19 01:30:24 +01:00
Release notes for #306
All checks were successful
Release note / release-note (pull_request) Successful in 17s
CI / build (pull_request) Successful in 5m32s
CI / e2e (pull_request) Successful in 12m23s
CI / promote (pull_request) Has been skipped
CI / build (push) Successful in 5m46s
CI / e2e (push) Successful in 12m20s
CI / promote (push) Successful in 31s
a364442126
Task: NEC-276

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J4oTDLYck3yQMFPJrLAA6j
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
Nectenda/nectenda!306
No description provided.