Skip to content

fix(lint): preserve Obsidian heading, block, and embed link syntax#180

Open
Aldominguez12 wants to merge 2 commits into
VectifyAI:mainfrom
Aldominguez12:fix/lint-obsidian-link-syntax
Open

fix(lint): preserve Obsidian heading, block, and embed link syntax#180
Aldominguez12 wants to merge 2 commits into
VectifyAI:mainfrom
Aldominguez12:fix/lint-obsidian-link-syntax

Conversation

@Aldominguez12

Copy link
Copy Markdown
Contributor

Problem

The wikilink regex in openkb/lint.py captured the whole [[...]] body as a single page target. Valid Obsidian syntax therefore never matched a known target:

  • [[page#Heading]] / [[page#^block]] — heading and block links
  • ![[file.png]] / [[report.pdf]] — attachment embeds and links
  • [[#Heading]] — same-page fragment links
  • ![[concepts/x]] — note embeds

Consequences:

  • openkb lint --fix destroyed valid links: strip_ghost_wikilinks demoted them to plain text. A wiki-wide sweep silently mangled hand-written notes in explorations/ — a data-loss path.
  • openkb lint reported them all as broken links (false positives).
  • Note embeds created no graph edges in visualize and could produce false orphans.

Fix

  • Parse the embed marker, target, fragment, and alias as named regex groups.
  • Validate only the page target; fragments survive fuzzy canonical rewrites ([[concepts/Gist_Memory#Notes]][[concepts/gist-memory#Notes]]).
  • Pass through attachment embeds/links (extension whitelist) and same-page [[#Heading]] links untouched.
  • Treat note embeds ![[concepts/x]] as regular page links: validated, rewritten keeping the !, counted as incoming links for orphan detection, and picked up as graph edges by visualize (which reuses _extract_wikilinks).

Verification

  • New TestObsidianSyntax coverage: heading/block/fragment preservation through direct and fuzzy matches, ghost demotion without ! residue, attachment/note embed handling, and a regression test asserting a hand-written explorations/ note survives a wiki-wide lint --fix byte-for-byte.
  • pytest tests/test_lint.py tests/test_lint_cli.py tests/test_visualize.py tests/test_chat_session.py tests/test_remove.py green; ruff check/format and mypy openkb/lint.py clean.

🤖 Generated with Claude Code

@Aldominguez12 Aldominguez12 force-pushed the fix/lint-obsidian-link-syntax branch from 9d33a56 to 2775a3a Compare July 12, 2026 05:30
The single-capture wikilink regex treated [[page#Heading]],
[[page#^block]], ![[file.png]] and [[report.pdf]] as whole page
targets. None of them can ever match a known target, so lint --fix
(strip_ghost_wikilinks) demoted valid Obsidian links to plain text —
silently destroying hand-written notes in explorations/ on a full
sweep — and find_broken_links reported them all as broken.

- Parse the embed marker, target, fragment and alias as named groups.
- Validate only the page target; fragments survive fuzzy canonical
  rewrites ([[concepts/Gist_Memory#Notes]] -> [[concepts/gist-memory#Notes]]).
- Pass through attachment embeds/links (extension whitelist) and
  same-page [[#Heading]] links untouched.
- Treat note embeds ![[concepts/x]] as regular page links: validated,
  rewritten keeping the embed marker, counted as incoming links for
  orphan detection and as graph edges in visualize.

Adds regression coverage including a hand-written explorations/ note
that must survive a wiki-wide lint --fix byte-for-byte.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@Aldominguez12 Aldominguez12 force-pushed the fix/lint-obsidian-link-syntax branch from 2775a3a to e8900ac Compare July 12, 2026 05:49
@KylinMountain

Copy link
Copy Markdown
Collaborator

Nice fix — this closes a real data-loss path and the test coverage is thorough.

One thing worth addressing before merge: _WIKILINK_RE backtracks O(n²) on long runs of unmatched [, and the new optional frag/alias groups make it ~10× slower than the old regex — a doc with [×20000 takes ~12s per scan (and lint scans each file twice), so a malformed/pathological doc (ASCII art, code fence, base64, stray LLM output) can hang the linter for seconds.

Excluding [ from the target class — (?P<target>[^\[\]|#]*) — makes it linear again, and as a bonus fixes [[[a]]] capturing [a as the target. The other edge cases I noticed are minor.

Exclude [ from the target character class. Allowing it made the scan
quadratic on long runs of unmatched brackets (20k chars took ~10s per
pass, and lint scans each file twice), and also let [[[a]]] capture
[a as the target instead of matching the inner [[a]].

Obsidian forbids [ in note names, so no legitimate link changes
behavior. Addresses review feedback on VectifyAI#180.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@Aldominguez12

Copy link
Copy Markdown
Contributor Author

Good catch — confirmed the quadratic behavior locally (20k unmatched [ took ~9.5s per scan, linear after the change) and that [[[a]]] now matches the inner [[a]] instead of capturing [a as the target. Fixed in e03e322 by excluding [ from the target class, with regression tests for both the pathological input and the nested-bracket case. All lint tests pass (88/88), ruff and mypy clean.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants