fix(watch): preserve unresolved authored reference edges (#3190)

Follow-up to #3191: an authored markdown link whose target basename is present in the graph
but under a different spelling (e.g. Windows-case [[target]] vs TARGET.md) was dropped on a
code-only rebuild. Preserve such an edge when its target node is still live in the merged
graph, without loosening the deleted-target existence gate (a genuinely deleted target's node
is evicted first, so it is never preserved).
This commit is contained in:
logan683 authored and safishamsi committed 2026-08-30 15:11:50 +01:00
1 parent 4d8cb4c803
commit 788c04e6c6
2 files changed
+184 -1

No files matched your search

+32 -1
View File
@@ -529,6 +529,7 @@ def _reconcile_markdown_links(
"""
from graphify.build import _is_ast_tier
from graphify.extract import _file_node_id, _safe_extract_with_xaml_root
from graphify.extractors.base import _make_id
from graphify.extractors.markdown import extract_markdown
all_nodes = result.get("nodes", []) + preserved_nodes
@@ -562,6 +563,7 @@ def _reconcile_markdown_links(
parsed_sources: set[str] = set()
authored_links: set[tuple[str, str]] = set()
authored_raw_pairs: set[frozenset[str]] = set()
unresolved_authored_sites: dict[tuple[str, str], set[str]] = {}
resolved_raw_pairs: dict[frozenset[str], frozenset[str]] = {}
desired_edges: list[tuple[str, str, dict, str, str]] = []
node_sources = {
@@ -583,7 +585,14 @@ def _reconcile_markdown_links(
extract_markdown, markdown_file, project_root
)
for edge in extraction.get("edges", []):
if edge.get("relation") != "references" or not edge.get("target_file"):
if edge.get("relation") != "references":
continue
if not edge.get("target_file"):
source_location = str(edge.get("source_location") or "")
if source_location:
unresolved_authored_sites.setdefault(
(source_file, source_location), set()
).add(str(edge.get("target") or ""))
continue
target_path = Path(edge["target_file"])
try:
@@ -620,6 +629,12 @@ def _reconcile_markdown_links(
for source_rep, target_rep, _, source_file, target_file in desired_edges
}
def _matches_unresolved_target(
raw_target: str, owner: str, target_source: str
) -> bool:
candidate = project_root / Path(owner).parent / Path(target_source).name
return raw_target == _make_id(str(candidate))
def _keep_edge(edge: dict) -> bool:
if not (_is_ast_tier(edge) and edge.get("relation") == "references"):
return True
@@ -627,6 +642,22 @@ def _reconcile_markdown_links(
if owner not in parsed_sources:
return True
unresolved_targets = unresolved_authored_sites.get(
(owner, str(edge.get("source_location") or "")), set()
)
if unresolved_targets:
target_sources = {
source
for endpoint in (edge.get("source"), edge.get("target"))
if (source := node_sources.get(endpoint)) and source != owner
}
if any(
_matches_unresolved_target(raw_target, owner, target_source)
for raw_target in unresolved_targets
for target_source in target_sources
):
return True
raw_pair = frozenset((edge.get("source"), edge.get("target")))
if raw_pair in authored_raw_pairs:
desired_pair = resolved_raw_pairs.get(raw_pair)
+152
View File
@@ -4051,3 +4051,155 @@ def test_markdown_reconcile_does_not_resurrect_a_deleted_target(tmp_path):
assert not any(
edge.get("target") == "b_sem" for edge in links
), "no edge should still point at the deleted document's node"
def test_markdown_reconcile_preserves_case_different_unresolved_target(tmp_path):
"""An unresolved authored site preserves its existing edge without guessing."""
original = _ast_reference(
"source_sem",
"target_sem",
"history/source.md",
sentinel="keep",
source_location="L3",
)
corpus, graph_path = _markdown_reconcile_fixture(
tmp_path,
{
"history/source.md": "# Source\n\nSee [[target]] for detail.\n",
"root/TARGET.md": "# Target\n\nBody.\n",
},
[
_semantic_doc("source_sem", "history/source.md"),
_semantic_doc("target_sem", "root/TARGET.md"),
],
[original],
)
assert _rebuild_code(corpus, no_cluster=True, acquire_lock=False) is True
links = json.loads(graph_path.read_text(encoding="utf-8"))["links"]
references = [edge for edge in links if edge.get("relation") == "references"]
assert references == [original]
def test_markdown_reconcile_prunes_removed_unresolved_link(tmp_path):
"""Removing the unresolved authored link still prunes its existing edge."""
corpus, graph_path = _markdown_reconcile_fixture(
tmp_path,
{
"history/source.md": "# Source\n\nSee nothing.\n",
"root/TARGET.md": "# Target\n\nBody.\n",
},
[
_semantic_doc("source_sem", "history/source.md"),
_semantic_doc("target_sem", "root/TARGET.md"),
],
[
_ast_reference(
"source_sem",
"target_sem",
"history/source.md",
sentinel="drop",
source_location="L3",
)
],
)
assert _rebuild_code(corpus, no_cluster=True, acquire_lock=False) is True
links = json.loads(graph_path.read_text(encoding="utf-8"))["links"]
references = [edge for edge in links if edge.get("relation") == "references"]
assert len(references) == 0
def test_markdown_reconcile_prunes_removed_colocated_unresolved_link(tmp_path):
"""One unresolved link does not preserve a removed neighbor on the same line."""
keep = _ast_reference(
"source_sem",
"target_sem",
"history/source.md",
sentinel="keep",
source_location="L3",
)
removed = _ast_reference(
"source_sem",
"removed_sem",
"history/source.md",
sentinel="removed",
source_location="L3",
)
corpus, graph_path = _markdown_reconcile_fixture(
tmp_path,
{
"history/source.md": "# Source\n\nSee [[target]] for detail.\n",
"root/TARGET.md": "# Target\n",
"root/REMOVED.md": "# Removed\n",
},
[
_semantic_doc("source_sem", "history/source.md"),
_semantic_doc("target_sem", "root/TARGET.md"),
_semantic_doc("removed_sem", "root/REMOVED.md"),
],
[keep, removed],
)
assert _rebuild_code(corpus, no_cluster=True, acquire_lock=False) is True
links = json.loads(graph_path.read_text(encoding="utf-8"))["links"]
references = [edge for edge in links if edge.get("relation") == "references"]
assert references == [keep]
def test_markdown_reconcile_does_not_suffix_match_unresolved_target(tmp_path):
"""An unresolved foo_target link does not preserve an old TARGET edge."""
stale = _ast_reference(
"source_sem",
"wrong_target_sem",
"history/source.md",
sentinel="must-prune",
source_location="L3",
)
corpus, graph_path = _markdown_reconcile_fixture(
tmp_path,
{
"history/source.md": "# Source\n\nSee [[foo_target]] for detail.\n",
"root/FOO_TARGET.md": "# Current target\n",
"root/TARGET.md": "# Removed target\n",
},
[
_semantic_doc("source_sem", "history/source.md"),
_semantic_doc("current_target_sem", "root/FOO_TARGET.md"),
_semantic_doc("wrong_target_sem", "root/TARGET.md"),
],
[stale],
)
assert _rebuild_code(corpus, no_cluster=True, acquire_lock=False) is True
links = json.loads(graph_path.read_text(encoding="utf-8"))["links"]
assert not any(edge.get("relation") == "references" for edge in links)
def test_markdown_reconcile_does_not_suffix_match_top_level_target(tmp_path):
"""A top-level foo_target link does not preserve an old TARGET edge."""
stale = _ast_reference(
"source_sem",
"wrong_target_sem",
"source.md",
sentinel="must-prune",
source_location="L3",
)
corpus, graph_path = _markdown_reconcile_fixture(
tmp_path,
{
"source.md": "# Source\n\nSee [[foo_target]] for detail.\n",
"TARGET.md": "# Removed target\n",
},
[
_semantic_doc("source_sem", "source.md"),
_semantic_doc("wrong_target_sem", "TARGET.md"),
],
[stale],
)
assert _rebuild_code(corpus, no_cluster=True, acquire_lock=False) is True
links = json.loads(graph_path.read_text(encoding="utf-8"))["links"]
assert not any(edge.get("relation") == "references" for edge in links)