NEC-286: Excalidraw embedded-file links in the folder-relative form, like canvas #298

Merged
nectenda-agent merged 20 commits from worktree-nec-286-excalidraw-embedded-file-links-in-the-fo into main 2026-10-16 00:54:54 +01:00
Collaborator

Task: NEC-286 — https://projectron.nerchure.com/tasks/299

Excalidraw drawings now hold an image link into the shared folder relative to that folder (:/rest), and each vault reads and writes it under its own folder name, on disk and in an open drawing (SAFE-A42, schema 2, like canvas's SAFE-A33). Rides along: NEC-289 (upgrade a structured document whose older client's fill arrives after this vault's first sync), NEC-290 (a schema upgrade runs only on a restamp, so it no longer retargets another vault's path outside its folder, for canvas and drawings), NEC-292 (a drawing's presence no longer outlives its closed tab), NEC-293 (an open drawing waits, live, for an image's file instead of reloading without it and warning 'could not find image file'; reconciled with NEC-291's let-go for a left-out link). Reviewed four times; local gate passed at e9fdd3ee; Excalidraw hardness run passed on seeds 1181215902, 2058022655, 1625222086, 1235200138 and 2034143022. The hardness run keeps the folder under one name in both vaults, so the two-name case rests on the unit tests and markdown-links.test.ts. Tested by the user in their own vaults.

Spec: docs/changes/NEC-286-excalidraw-embedded-paths/spec.md

Also fixes NEC-289: Upgrade a structured document's schema when an older client's fill arrives after this vault's first sync
Also fixes NEC-290: A schema upgrade rewrites another vault's path outside its shared folder when it begins with this vault's folder name
Also fixes NEC-292: A drawing's presence outlives its closed tab when the binding let go while attaching
Also fixes NEC-293: An open drawing reloads for an image before its attachment has arrived, and reports it not found

Changelog

Fixed an image in a synced Excalidraw drawing showing as an empty frame in a vault that keeps the shared folder under a different name.

Task: NEC-286 — https://projectron.nerchure.com/tasks/299 Excalidraw drawings now hold an image link into the shared folder relative to that folder ([[:/rest]]), and each vault reads and writes it under its own folder name, on disk and in an open drawing (SAFE-A42, schema 2, like canvas's SAFE-A33). Rides along: NEC-289 (upgrade a structured document whose older client's fill arrives after this vault's first sync), NEC-290 (a schema upgrade runs only on a restamp, so it no longer retargets another vault's path outside its folder, for canvas and drawings), NEC-292 (a drawing's presence no longer outlives its closed tab), NEC-293 (an open drawing waits, live, for an image's file instead of reloading without it and warning 'could not find image file'; reconciled with NEC-291's let-go for a left-out link). Reviewed four times; local gate passed at e9fdd3ee; Excalidraw hardness run passed on seeds 1181215902, 2058022655, 1625222086, 1235200138 and 2034143022. The hardness run keeps the folder under one name in both vaults, so the two-name case rests on the unit tests and markdown-links.test.ts. Tested by the user in their own vaults. Spec: `docs/changes/NEC-286-excalidraw-embedded-paths/spec.md` Also fixes NEC-289: Upgrade a structured document's schema when an older client's fill arrives after this vault's first sync Also fixes NEC-290: A schema upgrade rewrites another vault's path outside its shared folder when it begins with this vault's folder name Also fixes NEC-292: A drawing's presence outlives its closed tab when the binding let go while attaching Also fixes NEC-293: An open drawing reloads for an image before its attachment has arrived, and reports it not found ## Changelog Fixed an image in a synced Excalidraw drawing showing as an empty frame in a vault that keeps the shared folder under a different name.
An Excalidraw drawing names each image on a line under "## Embedded Files",
and those lines synced verbatim: a link from the vault root carried the
writer's local name for the shared folder, and a vault keeping the folder
under another name found the image only through SAFE-A41's guess. The lines
are whole values behind a codec, so they now take canvas's exact mechanism
(SAFE-A42): the document holds [[:/rest]], each vault reads and writes
[[<root>/rest]], and the open drawing's binding goes through the file's own
codec so a save is never read as an edit of the link. Schema 2, with an
upgrade like canvas's.

Not canvas's ./rest: a link can be relative to the drawing, and read as
folder-relative it would be written back as another file in every vault.

Task: NEC-286

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EKCzJG38W7mZSsoHpEggKb
StructuredSync offered a document to its codec's schema upgrade only at the
first sync. A vault that followed a new file from its listing could reach that
first sync while the document was still empty, and the older client's fill
then arrived as an update that nothing upgraded: its paths and links stayed in
the writer's own form here until this vault next connected. The update that
first stamps such a document now queues the same upgrade, in a transaction of
its own.

Task: NEC-286
Also-fixes: NEC-289

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EKCzJG38W7mZSsoHpEggKb
Task: NEC-286

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EKCzJG38W7mZSsoHpEggKb
A take-over's fill is written under an identity derived from the text, so two
vaults doing it at once write one fill (SAFE-A28). It went through the file's
own codec, which now writes a drawing's links in the form of where this vault
keeps the folder: two vaults keeping it in different places wrote two
different fills under one client id, and their documents never converged.
The fill now goes through the codec every vault shares, with the schema
version in its identity, and this vault's links are then made relative as its
own edit. Found in review.

Task: NEC-286

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EKCzJG38W7mZSsoHpEggKb
A codec's upgrade rewrites every path under this vault's root into the
folder-relative form, and it ran on every connect, whatever the stamp. A
document at this schema also holds, as written, paths other vaults wrote to
files outside their shared folder: a card or image link a vault sharing
`Team` made for its private `Shared/pic.png` was retargeted to
`Team/pic.png`, in every vault, once a vault sharing `Shared` connected.
The upgrade now runs only when restamping from an older schema. The cost:
paths a second older client wrote under another root stay as written.

Task: NEC-286
Also-fixes: NEC-290

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EKCzJG38W7mZSsoHpEggKb
A is bound live, so its view shows B's move before its file holds it; a check
of a file never rewritten would pass by itself. Found in review.

Task: NEC-286

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EKCzJG38W7mZSsoHpEggKb
An older client taking a drawing over at the same moment as this vault can
win the stamp: schema 1 over this vault's relative links, which that client
writes to disk as they are, and nothing upgraded the document again in this
session. A remote update that leaves the stamp below this schema now queues
the upgrade, whatever was checked before. A test now defends the schema
version in the take-over identity, and SAFE-A42 says only the filling vault's
links are made relative. Found in a second review.

Task: NEC-286

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EKCzJG38W7mZSsoHpEggKb
Task: NEC-286

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EKCzJG38W7mZSsoHpEggKb
A binding whose first draw let go — for an image its view had not loaded —
still returned "bound", so the manager kept it and started presence on it
after its detach had run. Nothing stopped that presence, and the next
binding's detach, seeing it still shared, only emptied its own: the user
stayed listed in every other vault after closing the tab. Found by the user.

Task: NEC-286
Also-fixes: NEC-292

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EKCzJG38W7mZSsoHpEggKb
An image inserted in one vault reaches another's open drawing live, ahead of
its attachment. The binding let go so the plugin reloaded for the picture,
while the file was still on its way: Excalidraw warned "could not find image
file", and the view, bound again with the image already in it, never
reloaded for the file once it came. Now the binding looks the image's link
up as Excalidraw does: a file here is reloaded for at once, as before, and
one not here is waited for, live, the image drawn empty, until Obsidian
reports it created; then the binding lets go once and the plugin loads it.
Nothing else waits, and a file that never arrives no longer keeps the
drawing reloading. Found by the user; the e2e check was shown to see the
warning by a run with the old behaviour put back.

Task: NEC-286
Also-fixes: NEC-293

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EKCzJG38W7mZSsoHpEggKb
Two gaps in waiting for a picture's file, found in review. A binding that let
go while waiting took the image in from its reload already drawn, so it never
waited again, and the file's arrival found no one listening: the image stayed
empty until the drawing was reopened. Every binding now waits, from the
moment it binds, for each image it shows unloaded whose file is not here.
And a changed line for an image the view already held was waited on like a
new one, so the view's next save would carry the old line back to every
vault; only a line the view never held waits now. The manager half of the
presence test now meets a binding that really lets go while binding.

Task: NEC-286
Also-fixes: NEC-293

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EKCzJG38W7mZSsoHpEggKb
Task: NEC-286

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EKCzJG38W7mZSsoHpEggKb
Main's let-go for a link a bound view's save leaves out (NEC-291) fired for a
picture whose file had not arrived, which is exactly when the view saves
without it: the plugin reloaded without the file and warned "could not find
image file" every 30 seconds, undoing NEC-293's wait. It now lets go only
where a reload would find the picture, and waits for the rest; the link
reaches the file when the picture arrives or the drawing closes. Also reads
the document there through the file's codec, as the rest of the binding does.

Task: NEC-286
Also-fixes: NEC-293

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EKCzJG38W7mZSsoHpEggKb
The waiting tests all used a bare link, the same in every form, so a lookup
in the document's folder-relative form, which never resolves, would have
passed them while waiting for good. Found in review.

Task: NEC-286

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EKCzJG38W7mZSsoHpEggKb
Record the hardness run after the rebase onto NEC-288 and NEC-291
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 19s
09476ba08b
Task: NEC-286

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EKCzJG38W7mZSsoHpEggKb
Release notes for the drawing link fixes and their ride-alongs
Some checks failed
Release note / release-note (pull_request) Successful in 18s
CI / build (pull_request) Failing after 39s
CI / e2e (pull_request) Failing after 1m0s
CI / promote (pull_request) Has been skipped
f3b9e71012
Task: NEC-286

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EKCzJG38W7mZSsoHpEggKb
Let the test's file-added hook keep its callable type
Some checks failed
Release note / release-note (pull_request) Successful in 19s
CI / build (pull_request) Failing after 1m42s
CI / e2e (pull_request) Successful in 12m16s
CI / promote (pull_request) Has been skipped
6ad81f807c
Assigned null inline, TypeScript narrowed it to null and refused the call,
which failed CI's build; the binding sets it when it starts waiting.

Task: NEC-286

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EKCzJG38W7mZSsoHpEggKb
Have the gesture test say whether its binding let go, and why
Some checks failed
Release note / release-note (pull_request) Successful in 18s
CI / build (pull_request) Failing after 2m2s
CI / promote (pull_request) Has been cancelled
CI / e2e (pull_request) Has been cancelled
cfffb4a7a5
It failed once on the CI runner under load at "on blur" with the previous
colour (PR #298), and never in 30 runs on a Mac. A binding that let go no
longer listens to the window; on failure the test now prints whether the
binding is bound, its refusal, and the log lines it wrote.

Task: NEC-286

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EKCzJG38W7mZSsoHpEggKb
Reset the runaway guard's counts before every test
Some checks failed
Release note / release-note (pull_request) Successful in 18s
CI / build (pull_request) Failing after 1m42s
CI / promote (pull_request) Has been cancelled
CI / e2e (pull_request) Has been cancelled
8696fd5a3a
The guard resets each count on a timer, and a test on fake timers can end
without running it: the count then leaked into every later test of the file
until the guard stopped a loop that was not running away, in whichever test
reached the limit. With this branch's added tests that was the NEC-211
gesture test on CI, which lost its binding mid-gesture (instrumented on the
runner). Test harness only; real timers are never dropped.

Task: NEC-286
Also-fixes: NEC-295

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EKCzJG38W7mZSsoHpEggKb
Wait for what the sync tests check, never only for a fixed settle
All checks were successful
Release note / release-note (pull_request) Successful in 19s
CI / e2e (pull_request) Successful in 12m6s
CI / build (pull_request) Successful in 5m54s
CI / promote (pull_request) Has been skipped
CI / build (push) Successful in 5m55s
CI / e2e (push) Successful in 12m16s
CI / promote (push) Successful in 33s
f99c227789
Two of this branch's sync tests read state after a fixed settle that the
loaded CI runner outran (PR #298): a document the older client had not yet
filled, and a file the other vault had not yet written. Each now waits for
the state it checks, and a check that something stays as written first waits
for the sync that could have changed it. Mutation-checked both ways.

Task: NEC-286

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EKCzJG38W7mZSsoHpEggKb
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!298
No description provided.