Skip to content

fix(markdown): resolve wikilinks vault-wide when sibling resolution misses - #2875

Closed
BaeHyunJae wants to merge 1 commit into
Graphify-Labs:v8from
BaeHyunJae:fix/markdown-wikilink-vault-fallback
Closed

fix(markdown): resolve wikilinks vault-wide when sibling resolution misses#2875
BaeHyunJae wants to merge 1 commit into
Graphify-Labs:v8from
BaeHyunJae:fix/markdown-wikilink-vault-fallback

Conversation

@BaeHyunJae

Copy link
Copy Markdown
Contributor

Problem

A [[wikilink]] is resolved lexically against the linking file's directory (_resolve_markdown_link), but Obsidian-style vaults resolve wikilinks by vault-global filename lookup: a note in a subfolder linking [[hub]] means the root-level hub.md. Today the lexically derived target id matches no node, the #2169 remap pass rightly refuses to canonicalize a nonexistent path, and the builder drops the edge — so every cross-folder vault link is a silently lost reference. That is the same lost-edge failure #1376 fixed for hub docs, reintroduced for the vault layout graphify's own Obsidian support targets.

Fix

When a wikilink's lexically resolved path does not exist, retry it against a per-scan-root index of linkable documents:

  • built lazily on first miss, per scan root, pruned by detect._SKIP_DIRS and dot-directories — the same corpus boundary the scanner draws (and Obsidian itself does not index dot-folders); cleared at the start of each extract() run beside the other run-scoped caches
  • NFC-normalized on both sides, so filesystems that disagree on Unicode normalization (macOS decomposes, others don't) still match
  • bare names match by basename, path-qualified targets ([[folder/name]]) by full segment suffix; ties break shallowest-first then lexicographically, mirroring Obsidian's root-wins collision rule, so resolution is deterministic regardless of walk order

A hit is a real file, so the existing target_file stamp and the #2169 remap canonicalize the edge on full and incremental runs alike — no new remap machinery. A miss stays dangling exactly as before. The fallback is scoped deliberately:

  • wikilinks only — vault-global lookup is a wikilink convention; inline [text](path.md) and reference-style links keep pure relative semantics, where a missing target is an authoring error rather than an alternate resolution order
  • only when the resolved path is missing — an existing sibling target still shadows the vault-wide match, so all currently-resolving links are untouched
  • only under an active extract() run — a direct extract_markdown() call has no scan root and behaves exactly as before

Tests

Six new tests in tests/test_languages.py (tmp_path style, end-to-end through extract()): subfolder→root resolution, path-qualified suffix match, root-wins collision, sibling-shadows-fallback, inline-links-unchanged, and NFD-link-to-NFC-file normalization. Full suite passes (the one pre-existing failure, test_hermes_skill_destination_posix_uses_home, fails identically on a clean checkout of v8 on Windows and is unrelated).

- 어휘적으로 해소된 경로가 부재한 위키링크만 스캔 루트 전역 파일명 색인으로 재해소하고, 인라인·참조
  링크와 실존 형제 대상은 종전 상대 의미론 유지.
- 색인은 detect._SKIP_DIRS 와 닷 디렉터리를 배제하고 NFC 정규화로 대조하며, 동명 충돌은 얕은 깊이
  우선 뒤 사전식 타이브레이크로 해소.
- 적중은 실파일이므로 target_file 스탬프와 기존 remap 경로가 전체·증분 실행 모두에서 정규화하고,
  미적중은 종전대로 매달린 간선 유지.

@graphify-labs graphify-labs Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Graphify reviewed this change.

Worth a look — the grounded gate found no coupling regressions or blocking issues, but 3 advisory finding(s) below merit a look before merge.

Formal verification. No changes could be formally verified in this run.


Graphify review — findings

Adds Obsidian-style vault-wide wikilink resolution to the markdown extractor: _resolve_markdown_link now takes a wikilink flag that, when a lexically-resolved sibling path doesn't exist, falls back to a per-scan-root basename index (_vault_lookup/_build_link_index) with shallowest-then-lexicographic tie-breaking and NFC normalization. Only wikilinks opt in — inline and reference-style links keep pure relative semantics. Wires _MD_LINK_INDEX_CACHE.clear() into extract() alongside the other per-run cache resets, and adds coverage for cross-folder, path-qualified, and root-wins-collision cases.

Worth a look

  • Markdown wikilink resolution reads a process-global active rootgraphify/extractors/markdown.py:112 · Escalate · medium
    • agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review Execution auto-disposal is off for this run; enable it (with sandbox isolation) to have Graphify try to confirm or refute this automatically.
  • Missing wikilinks now retarget to unrelated same-basename files under scan rootgraphify/extractors/markdown.py:221 · Escalate · medium
    • agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review Execution auto-disposal is off for this run; enable it (with sandbox isolation) to have Graphify try to confirm or refute this automatically.
  • Wikilink display-text alias not stripped before resolutiongraphify/extractors/markdown.py:374 · Escalate · medium
    • agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review Execution auto-disposal is off for this run; enable it (with sandbox isolation) to have Graphify try to confirm or refute this automatically.
Analysis details — impact, health, verification

Impact & health

Graphify review

Impact — 1849 functions depend on the 735 functions this change touches.

Health — this change adds coupling hotspots:

  • new: extract() — 473 callers, 42 callees
  • new: _rebuild_code() — 98 callers, 51 callees
  • new: extract_xaml() — 19 callers, 17 callees
  • new: extract_js() — 80 callers, 3 callees
  • new: dispatch_command() — 2 callers, 117 callees
  • new: _get_extractor() — 26 callers, 6 callees
  • new: run_pipeline() — 8 callers, 13 callees
  • new: extract_markdown() — 13 callers, 8 callees
  • …and 23 more — each is listed as a finding

Verification — 1849 functions in the blast radius were not formally verified this run (proofs are advisory here).

Gate & verification

graphify gate

PASS — objectively clean (no health regressions, tests not run — proofs not run this pass (advisory)). Grounded, not self-assessed.

Advisory (not blocking):

  • verification_scope: 1706 function(s) in the blast radius were not formally verified this run

Formal verification

Could not verify: Could not verify extract.

The verifier did not have enough to check extract, so it is saying so rather than guessing. No false assurance is the whole point.

Guarantee: No guarantee either way, this is an honest abstention, not a pass.

Note: Reason: parameter `cache_root` is annotated `Path | None` — outside the synthesizable primitive/collection set

Could not verify: Could not verify extract\_markdown.

The verifier did not have enough to check extract\_markdown, so it is saying so rather than guessing. No false assurance is the whole point.

Guarantee: No guarantee either way, this is an honest abstention, not a pass.

Note: Reason: parameter `path` is annotated `Path` — outside the synthesizable primitive/collection set

Could not verify: Could not verify \_resolve\_markdown\_link.

The verifier did not have enough to check \_resolve\_markdown\_link, so it is saying so rather than guessing. No false assurance is the whole point.

Guarantee: No guarantee either way, this is an honest abstention, not a pass.

Note: Reason: parameter `source_dir` is annotated `Path` — outside the synthesizable primitive/collection set

· 1 grounded finding(s) anchored inline below; 30 more finding(s) on lines outside this diff (see the check run).

return Path(os.path.normpath(str(hit)))
return resolved

def extract_markdown(path: Path) -> dict:

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Health regressionextract_markdown()

fans out to 8 callees (efferent coupling); 13 callers depend on it (afferent coupling).

Grounded coupling-delta finding (deterministic), not an LLM guess.

safishamsi pushed a commit that referenced this pull request Aug 20, 2026
…isses (#2875)

An Obsidian-style [[wikilink]] resolves to a note anywhere in the vault by basename, but
resolution only tried sibling files, so cross-folder links were silently lost. Add a
vault-wide fallback that fires only when relative/sibling resolution misses, with a
deterministic tiebreak on ambiguous basenames (shallowest path, then lexicographic
root-relative path) and a once-per-scan index so it stays O(N).
safishamsi added a commit that referenced this pull request Aug 20, 2026
Ships this cycle: the LLM-resilience batch — hollow-response same-chunk retry (#2880),
reasoning-first JSON recovery (#2882), deliberately-declined data JSON not counted as
failed (#2879); extractor fixes — C++ nested types + C++/CLI (#2876), markdown vault-wide
wikilinks (#2875); export fixes — control-char no longer aborts export (#2897), graph.html
restored for large graphs (#2853); and the --no-dedup opt-out (#2881).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@safishamsi

Copy link
Copy Markdown
Collaborator

Shipped in v0.9.48 via authorship-preserving cherry-pick. Thanks @BaeHyunJae! Release: https://github.com/Graphify-Labs/graphify/releases/tag/v0.9.48

@safishamsi safishamsi closed this Aug 20, 2026
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