fix(search): count frontmatter wikilinks in the link graph#151
Conversation
Link extraction only ran on the note body (parseNote(content).content), so [[wikilinks]] in frontmatter properties — notably `related:` — never entered the links table. This under-reported vault_get_backlinks and vault_get_outgoing_links and produced false orphans in vault_find_orphans for notes connected only via frontmatter (confirmed live: a session log whose only link was a related: wikilink returned 0 outgoing links). Add extractFrontmatterLinks() to pull wikilinks from all frontmatter property values (recursively across strings, arrays, nested objects), unioned with body links via extractAllLinks() at both indexing call sites (upsertNote + rebuildFromVault). Reuses the existing WIKILINK_RE and resolveLink — no new regex, resolution unchanged. Markdown links stay body-only. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016tczMk98trp1Log95f4x88
…ndex
upsertNote re-resolved stale link rows only for a new note's basename
("Note") and full path with extension ("folder/Note.md"), but folder
wikilinks ([[folder/Note]]) and markdown links store the target without the
extension ("folder/Note"). So a full-path link created before its target (via
the file watcher) never re-resolved until a full rebuild. Frontmatter related:
links are predominantly full-path, so the frontmatter extraction added in this
PR makes the gap common. Re-resolve the extensionless full-path form too.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016tczMk98trp1Log95f4x88
|
✅ Action performedReview finished.
|
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (3)
📝 WalkthroughWalkthroughAdds ChangesFrontmatter links as first-class graph edges
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Rename the recursive helper and its parameter so its role reads at a glance (collect -> collectWikilinksFrom, value -> frontmatterValue) and add inline comments for each branch and the non-obvious null guard. No behavior change. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016tczMk98trp1Log95f4x88
Function and helper names should state what they do specifically, and a docstring never excuses a vague name. The existing naming rule covers variable names (what a value is); this adds the function-name counterpart, a recurring review-churn point. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016tczMk98trp1Log95f4x88
note.path.replace(/\.md$/, "") states intent (remove the trailing .md) and mirrors the basename(note.path, ".md") on the line above, replacing slice(0, -3) and its explanatory comment. No behavior change. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016tczMk98trp1Log95f4x88
## Problem `resolveLink(target, allPaths)` was source-agnostic, so it resolved two of Obsidian's three "New link format" modes — **path from vault folder** (exact) and **shortest path** (basename) — but not **path from current file**. Relative links that traverse up or across folders (`[[../C/target]]`, `[text](../C/target.md)`) couldn't be resolved without knowing the linking note's location, so those edges silently dropped out of `vault_get_backlinks`, `vault_get_outgoing_links`, and `vault_find_orphans` (showing up as false orphans). Per the Obsidian-user parity principle (codified in AGENTS.md, included here): backlinks must include every link form Obsidian itself recognizes — a strict subset is a bug. ## Fix - `resolveLink` gains an optional `sourcePath`. When supplied, after the exact-path check it resolves the target against the linking note's directory via `posix.join(posix.dirname(sourcePath), target)` — which collapses `../`/`./`. A target that escapes the vault keeps a leading `..` and simply won't match any known path, so it returns null (no traversal risk). - Both indexing call sites (`upsertNote`, `rebuildFromVault` pass 2) pass the source note's path. - Resolution order: **exact vault-absolute → relative-to-source → basename/shortest-path**, so absolute paths still take precedence. ## Tests - 5 new `resolveLink` unit tests: upward `../` resolution, descending relative resolution (disambiguated against a shorter same-named path elsewhere, so it's locked to source-relative — not the pre-existing suffix match), exact-absolute precedence over a relative match, graceful null without a source, and null for a vault-escaping path. - 1 graph integration test: an `[[../Health/target]]` link across sibling folders now appears in both backlinks and outgoing links. - **Mutation-verified**: disabling the relative-to-source block turns exactly the 3 source-relative tests red while all others stay green. - Full suite **754 passing**; lint + build clean. ## Also included `docs(agents):` the **Obsidian-user parity principle** (it landed just after #151 merged, so it rides here as agreed) — the rationale behind both the frontmatter-link fix and this one. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_016tczMk98trp1Log95f4x88 --- _Generated by [Claude Code](https://claude.ai/code/session_016tczMk98trp1Log95f4x88)_ <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Bug Fixes** * Improved link resolution to support Obsidian-style relative paths (using `../` notation), path-from-current-file references, and all Obsidian link formats, ensuring consistent link resolution behavior across the vault. * **Documentation** * Clarified link resolution requirements and design principles for complete Obsidian compatibility. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: Claude <noreply@anthropic.com>
Problem
Link extraction only ever ran on the note body — both indexing call sites pass
parseNote(content).content, which strips frontmatter. As a result,[[wikilinks]]in frontmatter properties (notablyrelated:, which exists specifically to create graph edges like session→task traceability) never entered thelinkstable.This silently under-reported three tools:
vault_get_backlinks— missed notes referencing a target viarelated:vault_get_outgoing_links— missed a note's ownrelated:targetsvault_find_orphans— produced false orphans (notes connected only via frontmatter looked disconnected)Confirmed live:
vault_get_outgoing_linkson a session log whose only link is arelated:frontmatter wikilink returnedcount: 0. Obsidian resolves wikilinks in any frontmatter property, so the graph should too — this is a correctness bug, not a missing feature.Fix
extractFrontmatterLinks(data)recursively walks frontmatter property values (strings, arrays, nested objects) and pulls[[wikilink]]targets from every string. All properties are scanned, matching Obsidian.extractAllLinks(content, data)unions body + frontmatter links (deduped) so the two indexing paths (upsertNote+rebuildFromVault) can't diverge.WIKILINK_REandresolveLink— no new regex, resolution unchanged. Markdown[text](path.md)links stay body-only (frontmatter uses wikilink syntax).Tool descriptions are intentionally unchanged: they already describe links generically (
via incoming [[wikilinks]] or [markdown](links)) and never claimed body-only, so the fix aligns the implementation to existing copy.ARCHITECTURE.md(internal accuracy doc) is updated to note frontmatter as a link source.Tests
extractFrontmatterLinksunit tests (string/array/nested values, alias+heading stripping, embedded wikilink, dedup, empty cases).extractFrontmatterLinks → []turns exactly the 10 frontmatter-dependent tests red while all 152 pre-existing tests stay green — proving each new test depends on the fix and body-link behavior is unchanged.Follow-up
Unblocks a planned
vault_move_notetool (separate PR offmain), whose link-rewriting relies on a frontmatter-awaregetBacklinks.🤖 Generated with Claude Code
https://claude.ai/code/session_016tczMk98trp1Log95f4x88
Generated by Claude Code
Summary by CodeRabbit
New Features
related: [[note]]) are now recognized as valid graph connections and included in backlinks, outgoing links, and orphan detection.Documentation