Skip to content

fix(markdown): resolve same-page wikilink heading anchors (#3333) - #3450

Open
shivamtiwari3 wants to merge 1 commit into
Graphify-Labs:v8from
shivamtiwari3:fix/3333-wikilink-anchors
Open

fix(markdown): resolve same-page wikilink heading anchors (#3333)#3450
shivamtiwari3 wants to merge 1 commit into
Graphify-Labs:v8from
shivamtiwari3:fix/3333-wikilink-anchors

Conversation

@shivamtiwari3

Copy link
Copy Markdown

Addresses #3333.

The bug

_MD_WIKILINK_RE = re.compile(r'(?<!\!)\[\[([^\]|#]+)(?:[#|][^\]]*)?\]\]')
  1. [[#Heading]] never matched — the page-name group demanded at least one character before the #. A same-page anchor never entered the link list at all, so the whole [[#Heading]] convention was invisible to the graph.
  2. In [[Page#Heading|alias]] the non-capturing group swallowed everything from the # onward, so the fragment was discarded and the link could only resolve to Page.

The regex now captures the page name (empty for a same-page anchor) and the heading fragment separately, exactly as the report suggested.

Answering the reporter's question

does the project have a concept of heading-level nodes it would want this to resolve to, or should #Heading just get ignored on purpose?

It does — extract_markdown already emits a node per heading (h_nid = _make_id(stem, title)) with node_kind="heading". So a same-page anchor has a real, verifiable target, and this PR resolves it.

[[#Setup]] — and the long-hand [[ThisPage#Setup]], which the tgt_nid == file_nid self-reference guard used to drop outright — now produce a references edge to that heading's node.

Attributed to the enclosing section, not the file. [[#Setup]] written under ## Overview yields Overview --references--> Setup. Sourcing it from the file node would merely duplicate the existing file --contains--> Setup edge and carry no information; the enclosing-section rule is the same one heading nesting already uses (parent = heading_stack[-1][1] if heading_stack else file_nid).

Two safety properties:

  • Deferred to after the line walk — the heading may be declared below the link, so its node does not exist yet at the line the link is read on.
  • Existence-gated, like the document links above it: an anchor naming no heading in this file stays edge-free rather than fabricating a target. Self-anchors and repeats are deduped.

What this deliberately does not do

A fragment pointing into another file ([[Other#Details]]) keeps resolving to the page, as today. A heading id inside a file this per-file extractor has not parsed cannot be verified, and computing it speculatively would add dangling endpoints — the very symptom the report is trying to reduce. Doing that properly needs a corpus-level pass (like the existing raw_calls resolution), which felt out of scope for a regex fix. Happy to follow up if you'd want it.

Tests

tests/test_md_wikilink_anchors.py, 14 cases. Three assert the regex directly (same-page match with both groups; cross-page keeping both halves; plain/aliased/embed forms unchanged). The rest cover resolution: anchor → heading node; enclosing-section attribution; an anchor above its target; the long-hand same-page form; a nonexistent heading fabricating nothing; a self-anchor not becoming a self-loop; repeats deduped; a duplicate heading title resolving to the first (matching Obsidian); a cross-page anchored link still reaching the page; [[]] / [[|alias]] naming nothing; and an anchor inside a fenced block being ignored.

Full suite: 5493 passed, 3 pre-existing failures unrelated to this change (test_extract_code_only_cli, two test_ollama backend-detection tests — environment-dependent, failing identically on v8 without this patch).

🤖 Generated with Claude Code

@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 1 advisory finding(s) below merit a look before merge.

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


Graphify review — findings

Resolves Obsidian same-page heading anchors — both [[#Heading]] and the long-hand [[ThisPage#Heading]] — to the target heading's own node rather than dropping the fragment, by rewriting _MD_WIKILINK_RE to capture the page and anchor separately and matching an empty page name. Same-page anchors are collected during the walk and resolved afterward via pending_anchors, so a heading declared below its link still connects, with the edge attributed to the enclosing section. Resolution stays existence-gated: an anchor naming no real heading, a self-referential anchor, and repeated anchors in one section produce no spurious edges, while cross-page anchored links continue to resolve to the page.

Worth a look

  • Repeated same-page anchor references are collapsed to one linegraphify/extractors/markdown.py:447 · Escalate · medium
    • agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
Analysis details — impact, health, verification

Impact & health

Graphify review

Impact — 272 functions depend on the 251 functions this change touches.

Health — this change adds coupling hotspots:

  • new: extract_markdown() — 26 callers, 9 callees

Verification — 272 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: 272 function(s) in the blast radius were not formally verified this run

Test selection

Test selection

263 of 263 test file(s) selected (100%) via static blast radius.

Escalated to a full run for safety — the selection is not trustworthy on its own (see below). CI should run the whole suite.

  • tests/test_affected_cli.py — full-run-safety
  • tests/test_affected_member_seed.py — full-run-safety
  • tests/test_agents_platform.py — full-run-safety
  • tests/test_analyze.py — full-run-safety
  • tests/test_anthropic_custom_endpoint.py — full-run-safety
  • tests/test_antigravity_install.py — full-run-safety
  • tests/test_apm_fallback_version.py — full-run-safety
  • tests/test_architecture_doc.py — full-run-safety
  • tests/test_astro_extraction.py — full-run-safety
  • tests/test_astro_import_ids.py — full-run-safety
  • tests/test_atomic_canvas_export.py — full-run-safety
  • tests/test_atomic_version_stamp.py — full-run-safety
  • tests/test_atomic_writes.py — full-run-safety
  • tests/test_backend_extras.py — full-run-safety
  • tests/test_benchmark.py — full-run-safety
  • tests/test_benchmark_raw_graph.py — full-run-safety
  • tests/test_build.py — full-run-safety
  • tests/test_build_merge_hyperedges_and_prune.py — full-run-safety
  • tests/test_build_merge_shrink_guard.py — full-run-safety
  • tests/test_builtin_global_type_refs.py — full-run-safety
  • tests/test_cache.py — full-run-safety
  • tests/test_callflow_html.py — full-run-safety
  • tests/test_cargo_introspect.py — full-run-safety
  • tests/test_carried_hyperedge_remap.py — full-run-safety
  • tests/test_case_sensitive_resolution.py — full-run-safety
  • tests/test_charmap_encoding.py — full-run-safety
  • tests/test_chunking.py — full-run-safety
  • tests/test_cjs_module_extension.py — full-run-safety
  • tests/test_claude_cli_backend.py — full-run-safety
  • tests/test_claude_md.py — full-run-safety
  • tests/test_cli_broken_pipe.py — full-run-safety
  • tests/test_cli_export.py — full-run-safety
  • tests/test_cli_help.py — full-run-safety
  • tests/test_cluster.py — full-run-safety
  • tests/test_codebuddy.py — full-run-safety
  • tests/test_community_hub_labels.py — full-run-safety
  • tests/test_community_labels_skill.py — full-run-safety
  • tests/test_confidence.py — full-run-safety
  • tests/test_corrupt_graph_json.py — full-run-safety
  • tests/test_cpp_nested_and_cli.py — full-run-safety
  • tests/test_cpp_objc_cross_file_calls.py — full-run-safety
  • tests/test_cpp_preprocess.py — full-run-safety
  • tests/test_cross_extension_reexport_self_cycle.py — full-run-safety
  • tests/test_cross_language_call_resolution.py — full-run-safety
  • tests/test_cross_repo_member_calls.py — full-run-safety
  • tests/test_cross_repo_shared_types.py — full-run-safety
  • tests/test_csharp_call_site_generic_args.py — full-run-safety
  • tests/test_csharp_enum_members.py — full-run-safety
  • tests/test_csharp_field_generic_args.py — full-run-safety
  • tests/test_csharp_generic_callsites.py — full-run-safety
  • … and 213 more

non-code file(s) changed (CHANGELOG.md) → running the full suite for safety (a code graph can't see config/fixture/data deps)

changed code file(s) with no mapped test (CHANGELOG.md) — a coverage gap or a missing link — running the full suite rather than only the selected tests

Selection is safe under the controlled-regression assumption; always-run tests + a periodic full run are the backstops. Advisory — it never changes the check verdict.

Formal verification

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

· 1 more finding(s) on lines outside this diff (see the check run).

…abs#3333)

`_MD_WIKILINK_RE` was `\[\[([^\]|#]+)(?:[#|][^\]]*)?\]\]`, with two
defects:

1. `[[#Heading]]` never matched — the page-name group demanded at least
   one character before the `#`, so a same-page anchor never entered the
   link list and the convention was entirely invisible to the graph.
2. In `[[Page#Heading|alias]]` the non-capturing group swallowed
   everything from the `#` onward, so the fragment was discarded and the
   link could only ever resolve to `Page`.

The regex now captures the page name (empty for a same-page anchor) and
the heading fragment separately.

The extractor already emits a node per heading, so a same-page anchor has
a real target. `[[#Setup]]`, and the long-hand `[[ThisPage#Setup]]` that
the `tgt_nid == file_nid` guard used to drop outright, now resolve to that
heading's node — attributed to the section the link was written in, the
same parent rule heading nesting already uses, so an anchor is a genuine
section-to-section reference rather than a duplicate of the file's
`contains` edge.

Resolution is deferred to after the walk (the heading may be declared
below the link) and existence-gated: an anchor naming no heading in the
file stays edge-free rather than fabricating a target. Self-anchors and
repeats are deduped.

A fragment pointing into *another* file keeps resolving to the page, as
before: a heading id in a file this extractor has not parsed cannot be
verified, and guessing it would add dangling endpoints — the very symptom
the report is trying to reduce.
@shivamtiwari3
shivamtiwari3 force-pushed the fix/3333-wikilink-anchors branch from 6fd4e71 to 207b98c Compare September 10, 2026 05:15

@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 1 advisory finding(s) below merit a look before merge.

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


Graphify review — findings

Resolves Obsidian same-page heading anchors — [[#Heading]] and the long-hand [[ThisPage#Heading]] — to the heading's own node, attributed to the section the link sits in, by rewriting _MD_WIKILINK_RE to match an empty page name and capture the #fragment separately. Anchor resolution is deferred until the whole file is walked so a link above its target still connects, and is existence-gated: an anchor naming no heading (or its own section) produces no edge, duplicate anchors in one section collapse to one edge, and a duplicate heading title resolves to its first occurrence. Cross-page anchored links keep resolving to the page, since a heading id in another file can't be verified here.

Worth a look

  • Anchor resolution uses stem while document links resolve via full path idgraphify/extractors/markdown.py:447 · Escalate · high
    • agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
Analysis details — impact, health, verification

Impact & health

Graphify review

Impact — 272 functions depend on the 251 functions this change touches.

Health — this change adds coupling hotspots:

  • new: extract_markdown() — 26 callers, 9 callees

Verification — 272 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: 272 function(s) in the blast radius were not formally verified this run

Test selection

Test selection

263 of 263 test file(s) selected (100%) via static blast radius.

Escalated to a full run for safety — the selection is not trustworthy on its own (see below). CI should run the whole suite.

  • tests/test_affected_cli.py — full-run-safety
  • tests/test_affected_member_seed.py — full-run-safety
  • tests/test_agents_platform.py — full-run-safety
  • tests/test_analyze.py — full-run-safety
  • tests/test_anthropic_custom_endpoint.py — full-run-safety
  • tests/test_antigravity_install.py — full-run-safety
  • tests/test_apm_fallback_version.py — full-run-safety
  • tests/test_architecture_doc.py — full-run-safety
  • tests/test_astro_extraction.py — full-run-safety
  • tests/test_astro_import_ids.py — full-run-safety
  • tests/test_atomic_canvas_export.py — full-run-safety
  • tests/test_atomic_version_stamp.py — full-run-safety
  • tests/test_atomic_writes.py — full-run-safety
  • tests/test_backend_extras.py — full-run-safety
  • tests/test_benchmark.py — full-run-safety
  • tests/test_benchmark_raw_graph.py — full-run-safety
  • tests/test_build.py — full-run-safety
  • tests/test_build_merge_hyperedges_and_prune.py — full-run-safety
  • tests/test_build_merge_shrink_guard.py — full-run-safety
  • tests/test_builtin_global_type_refs.py — full-run-safety
  • tests/test_cache.py — full-run-safety
  • tests/test_callflow_html.py — full-run-safety
  • tests/test_cargo_introspect.py — full-run-safety
  • tests/test_carried_hyperedge_remap.py — full-run-safety
  • tests/test_case_sensitive_resolution.py — full-run-safety
  • tests/test_charmap_encoding.py — full-run-safety
  • tests/test_chunking.py — full-run-safety
  • tests/test_cjs_module_extension.py — full-run-safety
  • tests/test_claude_cli_backend.py — full-run-safety
  • tests/test_claude_md.py — full-run-safety
  • tests/test_cli_broken_pipe.py — full-run-safety
  • tests/test_cli_export.py — full-run-safety
  • tests/test_cli_help.py — full-run-safety
  • tests/test_cluster.py — full-run-safety
  • tests/test_codebuddy.py — full-run-safety
  • tests/test_community_hub_labels.py — full-run-safety
  • tests/test_community_labels_skill.py — full-run-safety
  • tests/test_confidence.py — full-run-safety
  • tests/test_corrupt_graph_json.py — full-run-safety
  • tests/test_cpp_nested_and_cli.py — full-run-safety
  • tests/test_cpp_objc_cross_file_calls.py — full-run-safety
  • tests/test_cpp_preprocess.py — full-run-safety
  • tests/test_cross_extension_reexport_self_cycle.py — full-run-safety
  • tests/test_cross_language_call_resolution.py — full-run-safety
  • tests/test_cross_repo_member_calls.py — full-run-safety
  • tests/test_cross_repo_shared_types.py — full-run-safety
  • tests/test_csharp_call_site_generic_args.py — full-run-safety
  • tests/test_csharp_enum_members.py — full-run-safety
  • tests/test_csharp_field_generic_args.py — full-run-safety
  • tests/test_csharp_generic_callsites.py — full-run-safety
  • … and 213 more

non-code file(s) changed (CHANGELOG.md) → running the full suite for safety (a code graph can't see config/fixture/data deps)

changed code file(s) with no mapped test (CHANGELOG.md) — a coverage gap or a missing link — running the full suite rather than only the selected tests

Selection is safe under the controlled-regression assumption; always-run tests + a periodic full run are the backstops. Advisory — it never changes the check verdict.

Formal verification

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

· 1 more finding(s) on lines outside this diff (see the check run).

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.

1 participant