Skip to content

fix(markdown): resolve wikilink same-page anchors and heading fragments - #3348

Open
SomSamantray wants to merge 2 commits into
Graphify-Labs:v8from
SomSamantray:fix/3333-wikilink-anchors
Open

fix(markdown): resolve wikilink same-page anchors and heading fragments#3348
SomSamantray wants to merge 2 commits into
Graphify-Labs:v8from
SomSamantray:fix/3333-wikilink-anchors

Conversation

@SomSamantray

Copy link
Copy Markdown

Summary

Obsidian wikilinks [[#Heading]] and [[Page#Heading|alias]] produced no graph edge before this change: the extractor's regex could not match a bare same-page anchor at all, and it discarded the heading fragment on anchored links. Entities referenced only through these forms stayed orphaned in the graph. Same-page anchors now resolve to that heading's node in the same file — resolved after the scan so forward references work, first occurrence wins for duplicated titles, and a heading that does not exist resolves to nothing rather than a dangling edge. Anchored cross-page links resolve to the page's node, matching the anchor-stripping semantics inline links already had, and dedup with their plain [[Page]] form.

Fixes #3333.

Validation: 7 new regression tests in tests/test_languages.py, written failing first (red on the pre-fix regex); full suite 5,241 passed / 0 failed with tests/test_skillgen.py excluded — its checks read git objects pinned at a historical ref that shallow local clones do not carry, and PR CI full-clones and covers them; ruff check clean; an end-to-end extract() run verified anchor edges merge into real nodes (no ghost endpoints).

Session-settled decisions carried from planning: PR process constraints — no Compound Engineering badge, no plan artifacts in the commit, repo contribution guidelines followed (user-directed, over the CE-branded pipeline default).

Unapplied review findings

  • P3 — graphify/extractors/markdown.py:433 — Shared linked_targets dedup lets a cross-page link steal a same-page anchor's target when the heading id collides with a file-node id (validated true; validator reproduced the silent one-edge drop)
    • suggested_fix: Use a separate dedup set for same-page anchor targets so a heading-target anchor and a file-target cross-page link can never suppress each other even when their ids coincide under make_id's recipe. Plus a regression test for the collision pair.

Review run context: run_id 20260904-230257-2c5b597a, artifact /tmp/compound-engineering-501/ce-code-review/20260904-230257-2c5b597a.

SomSamantray and others added 2 commits September 4, 2026 22:58
…nk fragments (Graphify-Labs#3333)

The wikilink regex discarded everything from # or | onward and could not
match a bare [[#Heading]] at all, so entities referenced only through
anchored links got no graph edge. Capture the heading fragment separately,
allow an empty page name, and resolve same-page anchors after the scan
against the first occurrence of that heading's node — a missing heading
resolves to nothing rather than dangling. Cross-page anchored links keep
inline-link semantics (anchor stripped, resolve to the page node) and dedup
with their plain form.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Post-scan anchor resolution now carries the same self-reference guard as
add_link so a heading id colliding with a file-node id can never emit a
self-loop; the fragment strip moves inside the wikilink-only branch; and
the new tests reuse the existing _md_extract helper, pin the dedup
behavior with a count assertion, and keep the mechanism comment to one
place. Behavior-preserving simplification from the pre-ship review pass.

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

@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.

Looks safe to merge — no coupling regressions and no blocking issues, checked against the code graph (not a self-assessment).

Formal verification. 3 change(s) tested, no difference found (not proven).


Graphify review — findings

Adds anchored-wikilink support to the Markdown extractor: [[Page#Heading|alias]] now resolves to Page's node (anchor stripped, deduped against a plain [[Page]] link), while a same-page [[#Heading]] resolves to that heading's node in the same file. Same-page anchors are deferred and resolved after the full scan against a first-occurrence-wins heading map, so a link may precede its target; a missing heading, a self-reference, or a degenerate form ([[]], [[#]], [[|alias]]) produces no edge rather than a dangling one.

No blocking issues surfaced. 5 lower-confidence candidates did not survive cross-model review.

Analysis details — impact, health, verification

Impact & health

Graphify review

Impact — 590 functions depend on the 590 functions this change touches.

Health — this change adds coupling hotspots:

  • new: extract_markdown() — 15 callers, 8 callees

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

Formal verification

No difference found (not proven): No behavior difference found in god\_nodes (not a proof).

The verifier ran both versions of god\_nodes on many inputs and saw identical behavior every time. Strong evidence the change is safe, but evidence, not a proof.

Guarantee: Empirical: differential testing (both versions run on many generated inputs). A divergence on an untested input remains possible, so this is 'no counterexample found', not 'proven equivalent'.

Note: An input the sampler did not try could still differ.

Could not verify: Could not verify build\_from\_json.

The verifier did not have enough to check build\_from\_json, 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: not verifiable: all 6 sampled inputs raised on both versions — the function never executed, so 'no divergence' would be vacuous (mostly NameError — names the real obstacle, not a sampling gap)

Could not verify: Could not verify \_absolutize\_source\_files\_in.

The verifier did not have enough to check \_absolutize\_source\_files\_in, 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 `root` is annotated `Path` — outside the synthesizable primitive/collection set

Could not verify: Could not verify \_relativize\_source\_files\_in.

The verifier did not have enough to check \_relativize\_source\_files\_in, 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 `root` is annotated `Path` — outside the synthesizable primitive/collection set

Could not verify: Could not verify dispatch\_command.

The verifier did not have enough to check dispatch\_command, 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: not verifiable: all 23 sampled inputs raised on both versions — the function never executed, so 'no divergence' would be vacuous (mostly SystemExit — names the real obstacle, not a sampling gap)

No difference found (not proven): No behavior difference found in to\_canvas (not a proof).

The verifier ran both versions of to\_canvas on many inputs and saw identical behavior every time. Strong evidence the change is safe, but evidence, not a proof.

Guarantee: Empirical: differential testing (both versions run on many generated inputs). A divergence on an untested input remains possible, so this is 'no counterexample found', not 'proven equivalent'.

Note: An input the sampler did not try could still differ.

Could not verify: Could not verify to\_obsidian.

The verifier did not have enough to check to\_obsidian, 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: non-vacuity: domain too small (only 2 distinct inputs exercised, need 3) — 'no divergence' would be near-vacuous

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 \_resolve\_cpp\_member\_calls.

The verifier did not have enough to check \_resolve\_cpp\_member\_calls, 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: non-vacuity: domain too small (only 1 distinct inputs exercised, need 3) — 'no divergence' would be near-vacuous

Could not verify: Could not verify \_resolve\_csharp\_member\_calls.

The verifier did not have enough to check \_resolve\_csharp\_member\_calls, 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: non-vacuity: domain too small (only 1 distinct inputs exercised, need 3) — 'no divergence' would be near-vacuous

Could not verify: Could not verify \_resolve\_java\_member\_calls.

The verifier did not have enough to check \_resolve\_java\_member\_calls, 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: non-vacuity: domain too small (only 1 distinct inputs exercised, need 3) — 'no divergence' would be near-vacuous

Could not verify: Could not verify \_resolve\_swift\_member\_calls.

The verifier did not have enough to check \_resolve\_swift\_member\_calls, 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: non-vacuity: domain too small (only 1 distinct inputs exercised, need 3) — 'no divergence' would be near-vacuous

Could not verify: Could not verify \_extract\_generic.

The verifier did not have enough to check \_extract\_generic, 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 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\_cross\_file\_imports.

The verifier did not have enough to check \_resolve\_cross\_file\_imports, 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: not verifiable: the input domain has 81 values but only 9 distinct were tested — a small finite domain must be EXHAUSTED, not sampled (an untested input could invert the result)

Could not verify: Could not verify global\_add.

The verifier did not have enough to check global\_add, 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_path` is annotated `Path` — outside the synthesizable primitive/collection set

Could not verify: Could not verify save\_query\_result.

The verifier did not have enough to check save\_query\_result, 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 `memory_dir` is annotated `Path` — outside the synthesizable primitive/collection set

Could not verify: Could not verify \_copy\_skill\_file.

The verifier did not have enough to check \_copy\_skill\_file, 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: not verifiable: all 23 sampled inputs raised on both versions — the function never executed, so 'no divergence' would be vacuous (mostly KeyError — names the real obstacle, not a sampling gap)

Could not verify: Could not verify vscode\_install.

The verifier did not have enough to check vscode\_install, 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: not verifiable: all 29 sampled inputs raised on both versions — the function never executed, so 'no divergence' would be vacuous (mostly NameError — names the real obstacle, not a sampling gap)

Could not verify: Could not verify \_parse\_cargo.

The verifier did not have enough to check \_parse\_cargo, 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: non-vacuity: domain too small (only 2 distinct inputs exercised, need 3) — 'no divergence' would be near-vacuous

Could not verify: Could not verify \_parse\_pyproject.

The verifier did not have enough to check \_parse\_pyproject, 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: non-vacuity: domain too small (only 2 distinct inputs exercised, need 3) — 'no divergence' would be near-vacuous

Could not verify: Could not verify \_build\_server.

The verifier did not have enough to check \_build\_server, 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: not verifiable: all 23 sampled inputs raised on both versions — the function never executed, so 'no divergence' would be vacuous

No difference found (not proven): No behavior difference found in \_node\_search\_text (not a proof).

The verifier ran both versions of \_node\_search\_text on many inputs and saw identical behavior every time. Strong evidence the change is safe, but evidence, not a proof.

Guarantee: Empirical: differential testing (both versions run on many generated inputs). A divergence on an untested input remains possible, so this is 'no counterexample found', not 'proven equivalent'.

Note: An input the sampler did not try could still differ.

Could not verify: Could not verify \_score\_query.

The verifier did not have enough to check \_score\_query, 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: not verifiable: all 45 sampled inputs raised on both versions — the function never executed, so 'no divergence' would be vacuous (mostly TypeError — names the real obstacle, not a sampling gap)

Could not verify: Could not verify \_rebase\_relative\_source\_files.

The verifier did not have enough to check \_rebase\_relative\_source\_files, 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_root` 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.

Wikilink regex drops same-page anchors ([[#Heading]]) and discards the #Heading fragment on cross-page anchored links

1 participant