mirror of
https://github.com/safishamsi/graphify.git
synced 2026-10-15 16:39:43 +00:00
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:
1 parent
4d8cb4c803
commit
788c04e6c6
2 files changed
+184
-1
No files matched your search
+32
-1
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
Reference in new issue
Block a user