Skip to content

fix(search): count frontmatter wikilinks in the link graph#151

Merged
aliasunder merged 5 commits into
mainfrom
claude/vault-bootstrap-setup-8bxnd2
Jun 18, 2026
Merged

fix(search): count frontmatter wikilinks in the link graph#151
aliasunder merged 5 commits into
mainfrom
claude/vault-bootstrap-setup-8bxnd2

Conversation

@aliasunder

@aliasunder aliasunder commented Jun 18, 2026

Copy link
Copy Markdown
Owner

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 (notably related:, which exists specifically to create graph edges like session→task traceability) never entered the links table.

This silently under-reported three tools:

  • vault_get_backlinks — missed notes referencing a target via related:
  • vault_get_outgoing_links — missed a note's own related: targets
  • vault_find_orphans — produced false orphans (notes connected only via frontmatter looked disconnected)

Confirmed live: vault_get_outgoing_links on a session log whose only link is a related: frontmatter wikilink returned count: 0. Obsidian resolves wikilinks in any frontmatter property, so the graph should too — this is a correctness bug, not a missing feature.

Fix

  • New extractFrontmatterLinks(data) recursively walks frontmatter property values (strings, arrays, nested objects) and pulls [[wikilink]] targets from every string. All properties are scanned, matching Obsidian.
  • A small private extractAllLinks(content, data) unions body + frontmatter links (deduped) so the two indexing paths (upsertNote + rebuildFromVault) can't diverge.
  • Reuses the existing WIKILINK_RE and resolveLink — 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

  • 8 extractFrontmatterLinks unit tests (string/array/nested values, alias+heading stripping, embedded wikilink, dedup, empty cases).
  • 4 graph integration tests + 1 rebuild forward-reference test. Each fixture's body contains no link to the target, so the asserted edge can only come from frontmatter extraction.
  • Mutation-verified: stubbing 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.
  • Full suite: 747 passed; lint + build clean.

Follow-up

Unblocks a planned vault_move_note tool (separate PR off main), whose link-rewriting relies on a frontmatter-aware getBacklinks.

🤖 Generated with Claude Code

https://claude.ai/code/session_016tczMk98trp1Log95f4x88


Generated by Claude Code

Summary by CodeRabbit

  • New Features

    • Frontmatter wikilinks (e.g., related: [[note]]) are now recognized as valid graph connections and included in backlinks, outgoing links, and orphan detection.
  • Documentation

    • Updated architecture documentation to reflect frontmatter link indexing behavior.

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
Comment thread src/vault-mcp/search/search-index.ts
…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
@aliasunder

Copy link
Copy Markdown
Owner Author

/@CodeRabbit

@coderabbitai

coderabbitai Bot commented Jun 18, 2026

Copy link
Copy Markdown

@aliasunder Sure! I'll review the changes in this PR right away.

✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai

coderabbitai Bot commented Jun 18, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 9eaa009c-125f-424e-a671-41d6ec0e8c39

📥 Commits

Reviewing files that changed from the base of the PR and between 45f0e02 and a0e9b2d.

📒 Files selected for processing (3)
  • ARCHITECTURE.md
  • src/vault-mcp/search/__tests__/search-index.test.ts
  • src/vault-mcp/search/search-index.ts

📝 Walkthrough

Walkthrough

Adds extractFrontmatterLinks, a recursive function that scans parsed frontmatter values for wikilinks, and extractAllLinks, which unions body and frontmatter targets with deduplication. Both upsertNote and rebuildFromVault pass 2 are updated to use extractAllLinks. The architecture doc and test suite are updated to match.

Changes

Frontmatter links as first-class graph edges

Layer / File(s) Summary
extractFrontmatterLinks and extractAllLinks implementation
src/vault-mcp/search/search-index.ts, ARCHITECTURE.md
Adds extractFrontmatterLinks to recursively walk frontmatter data and extract wikilink targets using the existing WIKILINK_RE, and extractAllLinks to deduplicate the union of body and frontmatter targets. Internal comment and architecture doc are updated to describe the expanded ingestion sources.
Wire extractAllLinks into upsertNote and rebuildFromVault
src/vault-mcp/search/search-index.ts
upsertNote incremental indexing switches from body-only extractLinks to extractAllLinks, and the stale-unresolved re-resolution logic gains a third target form (path without .md). rebuildFromVault pass 2 similarly calls extractAllLinks to include frontmatter edges during full rebuilds.
Unit and integration tests
src/vault-mcp/search/__tests__/search-index.test.ts
Adds a dedicated extractFrontmatterLinks unit suite covering strings, arrays, nested objects, alias/heading stripping, deduplication, and non-string scalars. Extends rebuild and forward-reference tests for frontmatter wikilinks. Adds a graph integration block asserting frontmatter-only edges appear in getOutgoingLinks/getBacklinks, orphan detection excludes frontmatter-connected notes, and body+frontmatter links produce a single edge.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main fix: adding frontmatter wikilink extraction to the link graph, addressing a correctness bug affecting backlinks, outgoing links, and orphan detection.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/vault-bootstrap-setup-8bxnd2

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

claude added 3 commits June 18, 2026 17:02
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
@aliasunder
aliasunder merged commit 413d349 into main Jun 18, 2026
15 checks passed
@aliasunder
aliasunder deleted the claude/vault-bootstrap-setup-8bxnd2 branch June 18, 2026 17:42
aliasunder added a commit that referenced this pull request Jun 18, 2026
## 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>
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