fix(markdown): resolve same-page wikilink heading anchors (#3333) - #3450
fix(markdown): resolve same-page wikilink heading anchors (#3333)#3450shivamtiwari3 wants to merge 1 commit into
Conversation
There was a problem hiding this comment.
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 line —
graphify/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-safetytests/test_affected_member_seed.py— full-run-safetytests/test_agents_platform.py— full-run-safetytests/test_analyze.py— full-run-safetytests/test_anthropic_custom_endpoint.py— full-run-safetytests/test_antigravity_install.py— full-run-safetytests/test_apm_fallback_version.py— full-run-safetytests/test_architecture_doc.py— full-run-safetytests/test_astro_extraction.py— full-run-safetytests/test_astro_import_ids.py— full-run-safetytests/test_atomic_canvas_export.py— full-run-safetytests/test_atomic_version_stamp.py— full-run-safetytests/test_atomic_writes.py— full-run-safetytests/test_backend_extras.py— full-run-safetytests/test_benchmark.py— full-run-safetytests/test_benchmark_raw_graph.py— full-run-safetytests/test_build.py— full-run-safetytests/test_build_merge_hyperedges_and_prune.py— full-run-safetytests/test_build_merge_shrink_guard.py— full-run-safetytests/test_builtin_global_type_refs.py— full-run-safetytests/test_cache.py— full-run-safetytests/test_callflow_html.py— full-run-safetytests/test_cargo_introspect.py— full-run-safetytests/test_carried_hyperedge_remap.py— full-run-safetytests/test_case_sensitive_resolution.py— full-run-safetytests/test_charmap_encoding.py— full-run-safetytests/test_chunking.py— full-run-safetytests/test_cjs_module_extension.py— full-run-safetytests/test_claude_cli_backend.py— full-run-safetytests/test_claude_md.py— full-run-safetytests/test_cli_broken_pipe.py— full-run-safetytests/test_cli_export.py— full-run-safetytests/test_cli_help.py— full-run-safetytests/test_cluster.py— full-run-safetytests/test_codebuddy.py— full-run-safetytests/test_community_hub_labels.py— full-run-safetytests/test_community_labels_skill.py— full-run-safetytests/test_confidence.py— full-run-safetytests/test_corrupt_graph_json.py— full-run-safetytests/test_cpp_nested_and_cli.py— full-run-safetytests/test_cpp_objc_cross_file_calls.py— full-run-safetytests/test_cpp_preprocess.py— full-run-safetytests/test_cross_extension_reexport_self_cycle.py— full-run-safetytests/test_cross_language_call_resolution.py— full-run-safetytests/test_cross_repo_member_calls.py— full-run-safetytests/test_cross_repo_shared_types.py— full-run-safetytests/test_csharp_call_site_generic_args.py— full-run-safetytests/test_csharp_enum_members.py— full-run-safetytests/test_csharp_field_generic_args.py— full-run-safetytests/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.
6fd4e71 to
207b98c
Compare
There was a problem hiding this comment.
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 id —
graphify/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-safetytests/test_affected_member_seed.py— full-run-safetytests/test_agents_platform.py— full-run-safetytests/test_analyze.py— full-run-safetytests/test_anthropic_custom_endpoint.py— full-run-safetytests/test_antigravity_install.py— full-run-safetytests/test_apm_fallback_version.py— full-run-safetytests/test_architecture_doc.py— full-run-safetytests/test_astro_extraction.py— full-run-safetytests/test_astro_import_ids.py— full-run-safetytests/test_atomic_canvas_export.py— full-run-safetytests/test_atomic_version_stamp.py— full-run-safetytests/test_atomic_writes.py— full-run-safetytests/test_backend_extras.py— full-run-safetytests/test_benchmark.py— full-run-safetytests/test_benchmark_raw_graph.py— full-run-safetytests/test_build.py— full-run-safetytests/test_build_merge_hyperedges_and_prune.py— full-run-safetytests/test_build_merge_shrink_guard.py— full-run-safetytests/test_builtin_global_type_refs.py— full-run-safetytests/test_cache.py— full-run-safetytests/test_callflow_html.py— full-run-safetytests/test_cargo_introspect.py— full-run-safetytests/test_carried_hyperedge_remap.py— full-run-safetytests/test_case_sensitive_resolution.py— full-run-safetytests/test_charmap_encoding.py— full-run-safetytests/test_chunking.py— full-run-safetytests/test_cjs_module_extension.py— full-run-safetytests/test_claude_cli_backend.py— full-run-safetytests/test_claude_md.py— full-run-safetytests/test_cli_broken_pipe.py— full-run-safetytests/test_cli_export.py— full-run-safetytests/test_cli_help.py— full-run-safetytests/test_cluster.py— full-run-safetytests/test_codebuddy.py— full-run-safetytests/test_community_hub_labels.py— full-run-safetytests/test_community_labels_skill.py— full-run-safetytests/test_confidence.py— full-run-safetytests/test_corrupt_graph_json.py— full-run-safetytests/test_cpp_nested_and_cli.py— full-run-safetytests/test_cpp_objc_cross_file_calls.py— full-run-safetytests/test_cpp_preprocess.py— full-run-safetytests/test_cross_extension_reexport_self_cycle.py— full-run-safetytests/test_cross_language_call_resolution.py— full-run-safetytests/test_cross_repo_member_calls.py— full-run-safetytests/test_cross_repo_shared_types.py— full-run-safetytests/test_csharp_call_site_generic_args.py— full-run-safetytests/test_csharp_enum_members.py— full-run-safetytests/test_csharp_field_generic_args.py— full-run-safetytests/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).
Addresses #3333.
The bug
[[#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.[[Page#Heading|alias]]the non-capturing group swallowed everything from the#onward, so the fragment was discarded and the link could only resolve toPage.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
It does —
extract_markdownalready emits a node per heading (h_nid = _make_id(stem, title)) withnode_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 thetgt_nid == file_nidself-reference guard used to drop outright — now produce areferencesedge to that heading's node.Attributed to the enclosing section, not the file.
[[#Setup]]written under## OverviewyieldsOverview --references--> Setup. Sourcing it from the file node would merely duplicate the existingfile --contains--> Setupedge 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:
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 existingraw_callsresolution), 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, twotest_ollamabackend-detection tests — environment-dependent, failing identically onv8without this patch).🤖 Generated with Claude Code