NEC-285: Sync Markdown Links doesn't work #295

Merged
nectenda-agent merged 6 commits from worktree-nec-285-sync-markdown-links-doesnt-work into main 2026-10-15 15:46:45 +01:00
Collaborator

Task: NEC-285 — https://projectron.nerchure.com/tasks/298

A link or embed written in one vault names a file by a path that includes that vault's local name for the shared folder (testOR/Untitled Kanban.md). In a vault that keeps the folder under another name it was unresolved, and clicking it created an empty note outside the shared folder; images and Excalidraw drawings broke the same way. Note text is shared byte for byte, so it is left as written: the plugin wraps Obsidian's public getFirstLinkpathDest and, when Obsidian finds nothing (or finds a file outside every shared folder, such as the empty note an earlier click made), reads the path as naming the folder by another vault's name and resolves it in this vault's copy, the source's own shared folder first. A file that is missing here, or skipped on this device, stays unresolved rather than resolving to another file of the same name. Canvas keeps its folder-relative form (SAFE-A33); the shared path helpers move into shared-paths.ts and new rule SAFE-A41 says which format uses which mechanism. Verified: unit tests with every rule mutation-checked, a new two-vault e2e (fails with the resolver off), the full gate, about an hour of Excalidraw hardness runs (one failure predates this change, replayed on main: NEC-288), and the user's own test in their vaults. Follow-ups: NEC-286, NEC-287.

Spec: docs/changes/NEC-285-sync-markdown-links/spec.md

Changelog

Links and embedded images that name a shared folder by another member's name for it now open the right note and show the right image, instead of creating an empty note.

Task: NEC-285 — https://projectron.nerchure.com/tasks/298 A link or embed written in one vault names a file by a path that includes that vault's local name for the shared folder ([[testOR/Untitled Kanban.md]]). In a vault that keeps the folder under another name it was unresolved, and clicking it created an empty note outside the shared folder; images and Excalidraw drawings broke the same way. Note text is shared byte for byte, so it is left as written: the plugin wraps Obsidian's public getFirstLinkpathDest and, when Obsidian finds nothing (or finds a file outside every shared folder, such as the empty note an earlier click made), reads the path as naming the folder by another vault's name and resolves it in this vault's copy, the source's own shared folder first. A file that is missing here, or skipped on this device, stays unresolved rather than resolving to another file of the same name. Canvas keeps its folder-relative form (SAFE-A33); the shared path helpers move into shared-paths.ts and new rule SAFE-A41 says which format uses which mechanism. Verified: unit tests with every rule mutation-checked, a new two-vault e2e (fails with the resolver off), the full gate, about an hour of Excalidraw hardness runs (one failure predates this change, replayed on main: NEC-288), and the user's own test in their vaults. Follow-ups: NEC-286, NEC-287. Spec: `docs/changes/NEC-285-sync-markdown-links/spec.md` ## Changelog Links and embedded images that name a shared folder by another member's name for it now open the right note and show the right image, instead of creating an empty note.
A link Obsidian writes with a path from the vault root carries the writer's
local name for the shared folder: [[testOR/Untitled Kanban.md]]. In a vault
keeping the folder as testicles2 it was unresolved, and clicking it created
an empty note outside the shared folder. Image embeds and Excalidraw's
embedded images broke the same way.

A note's text is shared byte for byte, so it is left as written. Instead
getFirstLinkpathDest is wrapped: when Obsidian finds nothing, a link from a
file in a shared folder is read as naming that folder by another vault's
name, and resolved in this vault's copy of the source's own folder first,
other shared folders only as best effort, ties left unresolved. Obsidian
and Excalidraw resolve through that one call, so clicks, rendering, embeds
and drawings follow. The skipped-attachment marker reads embeds the same
way.

Canvas keeps its folder-relative form (SAFE-A33): its file card looks the
file up exactly and never reaches link resolution. The path arithmetic both
mechanisms share moves into shared-paths.ts, and SAFE-A41 states the rule
and which format takes which mechanism.

Task: NEC-285

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CJ2Luoutm628qqYZ31niT5
The first version took the first reading of a link that was a file here.
Obsidian writes a path exactly when two files share a name, so when the
file a link meant was missing (not downloaded, too large, deleted), a
shorter reading landed on the other file of that name: the wrong image,
no "not downloaded" marker, and a rename here would have had Obsidian
rewrite the link to the wrong file in every vault.

Now the first reading that belongs in the folder decides: its file is
here, its subfolder is here, or the device knows it skipped it. A missing
file stays unresolved. The folder's root alone does not count, or no link
could reach another shared folder. The skipped-attachment marker reads an
embed the same way, so it names the file the link means.

The e2e now also checks that both kinds of link render as resolved, and
that a rename here, which makes Obsidian rewrite the link, leaves it
naming the same file in both vaults. Found by review.

Task: NEC-285

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CJ2Luoutm628qqYZ31niT5
main.ts is published in the mirror, which refuses a comment naming a
private doc.

Task: NEC-285

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CJ2Luoutm628qqYZ31niT5
Review expected Obsidian's link update on rename to rewrite a link once
the plugin resolves it. The e2e showed it does not: Obsidian collects the
links a rename should update with its internal getLinkpathDest, which the
plugin does not wrap. So a rename in a vault that names the folder
differently leaves the note's text untouched and the link stale in both
vaults. Kept that way by the user's choice, so the plugin never causes an
edit to shared text; the e2e now asserts it, and SAFE-A41 and
sync-limitations say so.

Task: NEC-285

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CJ2Luoutm628qqYZ31niT5
Follow a link past the empty note a click made before the fix
All checks were successful
Release note / release-note (pull_request) Successful in 22s
CI / build (pull_request) Successful in 6m22s
CI / e2e (pull_request) Successful in 12m48s
CI / promote (pull_request) Has been skipped
CI / build (push) Successful in 5m45s
CI / e2e (push) Successful in 12m10s
CI / promote (push) Successful in 33s
6d84698367
The user's own test of the fix still opened the stray note: the click
before the fix had made it at exactly the path the link names, outside
the shared folder, so Obsidian resolved to it and the resolver, acting
only when Obsidian found nothing, never ran. Every vault that hit the bug
holds such notes.

Now a link from a shared note that Obsidian resolves to a file outside
every shared folder goes to the shared copy when the reading finds one
there. The stray file is left exactly where it is. Chosen with the user;
the cost, a shared note's link to a private file lining up path for path
with a shared one, is in sync-limitations.

Task: NEC-285

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