Repository navigation
Feature: opt-in global-basename wikilink resolution for Obsidian-convention vaults #972
Description
Problem
When gbrain extract links ingests [[wikilinks]] from markdown files, the resolver only matches by:
- Parallel directory + same basename (e.g., a link from
originals/foo.mdresolves[[bar]]tooriginals/bar.md) - Same directory + sibling basename (e.g., a link from
concepts/foo.mdresolves[[bar]]toconcepts/bar.md)
It does not do a global-basename lookup. So a wikilink like [[struktura]] written in concepts/knowledge-graph.md does not resolve to projects/struktura.md, even though that page exists, has a unique basename in the vault, and resolves correctly in Obsidian.
This silently drops cross-directory bare-basename wikilinks. They're not flagged as broken — they're just missing from gbrain's graph layer.
Repro
# Set up a minimal vault with cross-directory link
mkdir -p /tmp/vault/{concepts,projects}
cat > /tmp/vault/projects/struktura.md <<EOF
---
title: Struktura
type: project
---
A project page.
EOF
cat > /tmp/vault/concepts/knowledge-graph.md <<EOF
---
title: Knowledge Graph
type: concept
---
This concept relates to [[struktura]].
EOF
gbrain init --force --dir /tmp/test-brain
GBRAIN_HOME=/tmp/test-brain gbrain import /tmp/vault
GBRAIN_HOME=/tmp/test-brain gbrain extract links
GBRAIN_HOME=/tmp/test-brain gbrain stats
# Links: 0 (expected: 1)Obsidian's graph view correctly links the two nodes. Gbrain's graph layer does not.
Why it matters
For users who follow Obsidian's standard convention (flat-namespace wikilinks, globally-unique basenames), this silently drops most of their interlinking from gbrain's graph layer. We hit this on a fresh wiki: 71 wikilinks total across 20 pages in Obsidian (avg 3.55/page, dense galaxy), but gbrain's extract links only produced 12 — all of which were same-directory. The remaining ~60 cross-directory bare-basename links were dropped silently.
Symptoms downstream:
gbrain graph <slug>returns thin or missing edges that exist in the source filesgbrain backlinks <slug>undercounts incoming references- Graph-traversal queries (e.g., "what concepts touch this project") return incomplete neighbors
- Semantic search via
gbrain queryis unaffected (works on chunks/embeddings, not the graph layer) — but anyone relying on graph traversal gets a partial view
Why it's probably intentional (and what to do anyway)
Gbrain's parent/ancestor resolution suggests it's designed for monorepo-style knowledge bases where basename collisions across folders are common, and path disambiguation is necessary. That's a defensible design choice for one user population. But Obsidian convention is the opposite — flat namespace, globally-unique basenames, bare wikilinks everywhere. The two assumptions clash.
Proposed: add an opt-in "global basename" resolution mode
# gbrain.yml or config
link_resolution:
mode: ancestor # current default — backward compatible
# OR
mode: global_basename # treat bare wikilinks as referring to any uniquely-named page in the vaultIn global_basename mode, resolveSlug("struktura", from_page) would:
- Try parent/ancestor lookup first (current behavior)
- Fall back to: search
allSlugsfor any slug whose basename matches - If exactly one match → resolve to it
- If multiple matches → ambiguous, drop with a warning (so the user knows to disambiguate via path prefix)
- If zero matches → drop silently (existing behavior)
Default stays ancestor for back-compat. Users with flat-namespace vaults flip to global_basename in their config.
Alternative — auto-detect from gbrain doctor
gbrain doctor could count bare-basename wikilinks that fail to resolve under ancestor mode but WOULD resolve under global mode, and surface that as a hint:
ℹ️ Detected 60 wikilinks that would resolve under global_basename mode but currently drop.
Your vault appears to use Obsidian-flat-namespace convention.
Set `link_resolution.mode: global_basename` in gbrain.yml to enable.
Severity
Medium. Doesn't break anything (semantic search works fine), but silently undercounts the graph for Obsidian-convention users, leading to thin/wrong graph traversal results. Easy upstream fix; current workarounds (path-prefixed wikilinks everywhere) are ugly enough that most users would rather accept the gap than adopt them.
Affected file
src/core/link-extraction.ts — the resolveSlug function around line 160-192.
Diagnosed by an agent
This was caught by an LLM agent (Hermes-driven Echo) doing read-after-write verification on a freshly-seeded wiki, then reading gbrain's source to confirm the resolver mechanism. Worth noting as evidence that the verify-before-done discipline catches real architectural gaps that wouldn't show up in CLI smoke tests.
Activity
Merge origin/master into fix-972-global-basename-wikilinks
docs: document opt-in global-basename wikilink resolution (garrytan#972)
Merge origin/master (v0.42.5.0) into fix-972-global-basename-wikilinks
fix(garrytan#972): resolve aliased wikilinks by target slug, not disp…
fix(garrytan#972): reconcile wikilink-resolved edges in put_page auto…
fix(garrytan#972): source-scope basename resolution (no cross-source …
fix(garrytan#972): FS-source basename edges carry link_source='wikili…
refactor(garrytan#972): one shared basename matcher across resolver/F…
fix(garrytan#972): P2 cluster — masking, code-fence, self-link, dedup…
perf(garrytan#972): bound the doctor link_resolution_opportunity scan
docs(garrytan#972): reconcile stale version/migration references to v…
Merge origin/master (v0.42.8.0) into fix-972-global-basename-wikilinks
fix(garrytan#972): register doctor check category + bump llms budget …
v0.42.10.0 feat(extract): opt-in global-basename wikilink resolution (c…
3 remaining items
docs(ledger): TIM-106 status — reconcile done; cutover gated on link-…
TIM-107: re-port link-resolution patches (PARA dirs + alias/title) on…
feat(links): resolve [[wikilink]] frontmatter values via global_basename
fix(links): resolve path-qualified wikilinks outside DIR_PATTERN in t…
fix(links): resolve path-qualified wikilinks outside DIR_PATTERN in t…
fix(extract): --stale sweep runs the real resolver — basename resolut…
fix(extract): --stale sweep runs the real resolver — basename resolut…
fix(extract): --stale sweep runs the real resolver — basename resolut…
feat(links): resolve [[wikilink]] frontmatter values via global_basen…
fix(extract): --stale sweep runs the real resolver — basename resolut…
fix(links): resolve path-qualified wikilinks outside DIR_PATTERN in t…
feat(links): resolve [[wikilink]] frontmatter values via global_basen…
fix(extract): --stale sweep runs the real resolver — basename resolut…
feat(links): resolve [[wikilink]] frontmatter values via global_basen…
fix(links): fold stroke letters in normalizeBasename, share the table