NEC-302: Three vaults can converge on a properties block whose closing delimiter is glued to a value #305

Merged
nectenda-agent merged 12 commits from worktree-nec-302-three-vaults-can-converge-on-a-propertie into main 2026-10-18 20:48:12 +01:00
Collaborator

Task: NEC-302 — https://projectron.nerchure.com/tasks/321

Rewrites of a note's properties block never cross its delimiters again. NEC-26's 1 October departures, which rebuilt a broken block whole from the map and inferred older-plugin removals from merged text, let vaults rewriting from different states delete or glue the --- lines (NEC-302), lose values (NEC-304), invent property names (NEC-305) and leave a second block (NEC-306). Now a rewrite only edits between an intact opening line and closing ---; a missing block is inserted at the top; stranded text is moved below the block; a glued closing line is found again; a closing --- gone altogether is refused with a notice and the values kept. Older-plugin adopt/prune runs only when nothing from a newer plugin arrived with it, and every prune is backed up. NEC-303 (one derived id authored as two structs) is fixed by taking identity from Yjs placement. The property test now really runs 40-op sequences (size: 'max') and checks the frame after every projection. Spec: docs/changes/NEC-302-glued-closing-delimiter/spec.md.

Spec: docs/changes/NEC-302-glued-closing-delimiter/spec.md

Also fixes NEC-303: A frontmatter projection that inserts can be authored as two different structs under one derived id
Also fixes NEC-304: Long edit sequences can leave every vault with a properties block that has lost its closing delimiter
Also fixes NEC-305: Concurrent property edits can converge on property names nobody set
Also fixes NEC-306: Two vaults adding a properties block at once leave a stale second block in the body

Changelog

Fixed cases where a note's properties, changed in several vaults at once, could end up unreadable, duplicated, or missing a value.

Task: NEC-302 — https://projectron.nerchure.com/tasks/321 Rewrites of a note's properties block never cross its delimiters again. NEC-26's 1 October departures, which rebuilt a broken block whole from the map and inferred older-plugin removals from merged text, let vaults rewriting from different states delete or glue the --- lines (NEC-302), lose values (NEC-304), invent property names (NEC-305) and leave a second block (NEC-306). Now a rewrite only edits between an intact opening line and closing ---; a missing block is inserted at the top; stranded text is moved below the block; a glued closing line is found again; a closing --- gone altogether is refused with a notice and the values kept. Older-plugin adopt/prune runs only when nothing from a newer plugin arrived with it, and every prune is backed up. NEC-303 (one derived id authored as two structs) is fixed by taking identity from Yjs placement. The property test now really runs 40-op sequences (size: 'max') and checks the frame after every projection. Spec: docs/changes/NEC-302-glued-closing-delimiter/spec.md. Spec: `docs/changes/NEC-302-glued-closing-delimiter/spec.md` Also fixes NEC-303: A frontmatter projection that inserts can be authored as two different structs under one derived id Also fixes NEC-304: Long edit sequences can leave every vault with a properties block that has lost its closing delimiter Also fixes NEC-305: Concurrent property edits can converge on property names nobody set Also fixes NEC-306: Two vaults adding a properties block at once leave a stale second block in the body ## Changelog Fixed cases where a note's properties, changed in several vaults at once, could end up unreadable, duplicated, or missing a value.
Two vaults settling one property clash from different merges could each
rebuild the block by deleting a different copy of the value. Merged, the
two deletions removed both, gluing the closing delimiter to the emptied
property in every vault. A deletion authors no struct, so neither vault
receiving it knew a projection had arrived, and none repaired it.

A projection that only deletes now also writes a meta entry under its
derived identity, in the same update. Projections that insert are left
unmarked, so their identity still depends on text and map alone.

Task: NEC-302

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lfzs4vox8ptLyatpPiC5Mg
Review found that dropping the overwritten entry from a mark's signature
failed no test, though it is what keeps two vaults that saw different
marks from writing two marks under one identity. Add the test.

Also record that a vault on an earlier build still sends unmarked
deletions, so the glued-delimiter case remains possible with one in the
mix, and correct the spec's claim about what earlier builds do with the
mark.

Task: NEC-302

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lfzs4vox8ptLyatpPiC5Mg
A projection's identity was derived from the visible characters either
side of the edit. Yjs places an insertion by the items either side,
deleted ones included, so two vaults that agreed on the visible
characters but held different deleted ones between them wrote one
identity as two different structs. Each kept its own, with state
vectors that said they agreed.

An inserting projection is now made once in a throwaway copy under an
unused identity, to read the origins Yjs gives it, and its identity is
derived from those, the characters it removes and its text. The
three-vault property test now also checks that every vault holds the
same struct under every projection's identity.

Task: NEC-302
Also-fixes: NEC-303

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lfzs4vox8ptLyatpPiC5Mg
Review of the NEC-303 fix: two vaults that differ only in deleted
characters at the insertion point now insert under two identities, so
the value is briefly there twice until reconcile rebuilds the block,
backed up first. Say so, and that the old bug could leave texts
different rather than only structs. Bring the identity's doc comment
and SAFE-A24 in line with what is hashed.

Task: NEC-302
Also-fixes: NEC-303

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lfzs4vox8ptLyatpPiC5Mg
Reconcile adopted a property the text held and pruned one it lacked as
an older client's edit whenever no map-writing client had touched that
key. Merges among newer clients alone produce both: a removed value
glued onto the next key (`atitle`) was adopted in every vault, and a
line deleted by a removal that lost, by a whole-block rewrite or by the
block's delimiters going was pruned, dropping values nobody removed with
no copy and no backup. Two concurrent edits were enough.

Adopt now needs a character an older client wrote in the line. Prune
runs only when nothing from a map-writing client arrived with the
change, and only for a property whose line's first character has been
deleted. A conflict copy is now checked to hold the values it keeps; one
built by a projection could claim a value the text no longer had.

The three-vault property test now generates the lengths it claims and
checks that no value is silently lost and no property appears that no
edit used. An older client's removal arriving with a newer client's work
can now be written back once, backed up, before it settles.

Task: NEC-302
Also-fixes: NEC-304
Also-fixes: NEC-305

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lfzs4vox8ptLyatpPiC5Mg
Two rewrites that each removed the block, from different states, could
between them delete both of its delimiters, and every vault was left on
an unclosed block with nothing to say where the body began. The body's
first character, remembered by identity, now says where it begins, and
the block is rebuilt up to it, backed up first. An empty block qualifies
too.

Prune keeps its check that the property's line's first character was
deleted: removing an emptied block deletes only its delimiters, a
newer client's pure deletion carries no author, and without the check a
value set meanwhile was pruned with no copy.

Each new rule is defended by a deterministic regression and was
mutation-checked: adopting without authorship, pruning without the
writer gate or the line check, rebuilding without the body start, and a
copy trusted without checking all fail. SAFE-A23 and SAFE-A25, the
limitations and the spec now say what the code does.

Task: NEC-302
Also-fixes: NEC-304
Also-fixes: NEC-305

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lfzs4vox8ptLyatpPiC5Mg
Review of the reconcile changes found two regressions and a remaining
loss. An older client's change that only deletes characters, arriving
on its own, was no longer adopted: authorship is now required only when
a map-writing client acted in what arrived. A block whose closing
delimiter the user had taken out by hand was rebuilt under them: the
remembered body start is now used only where nothing before it holds a
value. A newer client's text-only edit can still look like an older
client's removal, so every prune is now backed up holding the value it
removes.

Also removes the stale second block two vaults leave when each gives a
note its first properties at once, which became the note's properties
when the last one was removed. It is deleted by position, as an edit of
its own: folded into a whole-region rewrite, two vaults diffing two
near-identical blocks picked different characters and between them lost
a delimiter.

The property test now counts an edit that wrote anything, readable or
not. Every new rule has a deterministic regression and fails its tests
when broken. SAFE-A23, SAFE-A25, the limitations and the spec now claim
only what the code does.

Task: NEC-302
Also-fixes: NEC-304
Also-fixes: NEC-306

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lfzs4vox8ptLyatpPiC5Mg
Review: removing a leftover second block deleted a body that opens with
a block of its own, in every vault. A leftover is now only a block
written at the very start of the note, as both blocks are when two
vaults each create one at once; text written after the real block is
the note's own.

Review: the rule that a block with no closing delimiter is rebuilt from
the remembered body start only where nothing before it holds a value
left most merge residues unrepaired. It is replaced by when the
delimiter went: a block that read before what is arriving now, and does
not after, was broken by that arrival, and is rebuilt; one already open
here was opened by someone and is left to them. A second anchor, the
body's first non-blank character, stands in when a rewrite took the
blank line that began the body.

Task: NEC-302
Also-fixes: NEC-304
Also-fixes: NEC-306

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lfzs4vox8ptLyatpPiC5Mg
Review of the NEC-26 design behind NEC-302 to NEC-306 found the cause in
how broken blocks were rebuilt: whole, with the edit worked out by
diffing text, so two vaults rebuilding from different states could
between them delete a delimiter, the line break before one, or
everything in front of the opening one. Each patch since made that
rarer without removing it.

Every rewrite is now confined: it replaces only what lies between a
block's opening line and its closing `---`, worked out without the line
break before the closing one, so whichever characters a diff picks they
are inside the block. A rewrite never removes a block; an emptied one
stays. Repairs of what people's own merged edits break only insert or
rewrite in front of an intact delimiter: a missing block is inserted at
the top, characters glued in front of the opening line are moved after
the block, and a closing `---` with text glued onto its line is found
again by identity or by every line above being a property. A closing
`---` gone altogether is refused, as NEC-26 first said, with a notice.

Older-plugin inference runs only when nothing from a map-writing client
arrived with the change; authorship scanning, the body-start rebuild
and its gating are gone.

The test vault now checks every rewrite against the frame by identity,
and the property test accepts a block people's edits broke only when no
rewrite broke a frame and every value is kept. 30,000 runs of up to 40
edits pass; ten mutations, each breaking one rule, all fail.

Task: NEC-302
Also-fixes: NEC-304
Also-fixes: NEC-305
Also-fixes: NEC-306

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lfzs4vox8ptLyatpPiC5Mg
SAFE-A23 now says a rewrite never deletes a delimiter, the line break
before the closing one, or a block, and which repairs of what people's
merged edits break are allowed: inserting a missing block, moving
characters stranded in front of the opening line, and finding a glued
closing delimiter again. A closing delimiter gone altogether is refused.
SAFE-A24 confines a projection to the inside of a block; SAFE-A25 adopts
and prunes only when nothing from a map-writing client arrived.

The limitations, the component doc and the module header describe the
design as built, and the NEC-26 spec gains a dated correction: its
30,000 runs of "up to 40 operations" never held more than 10.

Task: NEC-302
Also-fixes: NEC-304
Also-fixes: NEC-305
Also-fixes: NEC-306

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lfzs4vox8ptLyatpPiC5Mg
Review of the frame rework found three faults.

A vault set a value in a block another vault's Obsidian was removing at
that moment; the merge left the value unreadable in front of the next
block, Obsidian then wrote a fresh block on top, and capture took every
property missing from it as removed, dropping the value with no copy.
Capture now removes nothing when the note's properties did not read just
before the edit.

Unstranding returned before settling the block. Every vault unstranding
one state makes one edit, which gave nobody anything to reconcile on, so
a `doingdone` merge stayed. The block is now settled in the same pass.

The test vault's frame check took one snapshot for two projections made
in one pass, so the first could never fail, and only some tests asserted
it. It now snapshots each transaction and every `sync` asserts it.

Also corrects SAFE-A25's lead sentence and two overclaims in the
limitations, and lists two cases, unchanged by NEC-302, in which a rule
in the body is taken for the closing line.

Task: NEC-302
Also-fixes: NEC-304
Also-fixes: NEC-306

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lfzs4vox8ptLyatpPiC5Mg
Count a property backspaced away once its line is gone
All checks were successful
Release note / release-note (pull_request) Successful in 20s
CI / build (pull_request) Successful in 5m31s
CI / e2e (pull_request) Successful in 12m22s
CI / promote (pull_request) Has been skipped
CI / build (push) Successful in 5m50s
CI / e2e (push) Successful in 12m21s
CI / promote (push) Successful in 31s
f8e1630d4b
The previous fix stopped capture from removing a property after an edit
to a note whose properties did not read, which also stopped a property
deleted key by key in source mode from being removed: backspacing passes
through states that do not read, and the next projection wrote it back
in every vault.

A removal after an unreadable state now counts once the property line's
first character is deleted, and a property not yet removed stays in the
base for the next edit, so a later keystroke still sees it go. A test
types four deletion orders keystroke by keystroke, both sent as typed
and all at once; removing either half of the fix fails it.

Task: NEC-302
Also-fixes: NEC-304

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lfzs4vox8ptLyatpPiC5Mg
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
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!305
No description provided.