diff --git a/graphify/cache.py b/graphify/cache.py index cc3a5a48b..63afc98ee 100644 --- a/graphify/cache.py +++ b/graphify/cache.py @@ -1592,6 +1592,24 @@ def save_semantic_cache( for fpath, result in by_file.items(): cache_path, p = _recover_group_path(fpath) if p.is_file(): + if cache_path != source_path(fpath): + # #2973: recovery only redirected the WRITE KEY. Each item in + # this group still carries the original unresolvable + # source_file string, and _semantic_entry_matches_path + # rejects an entry on read if any item's source_file doesn't + # match the path it was loaded under -- so leaving the old + # value in place would write a "successful" entry that can + # never actually be read back, silently reproducing the same + # loss this recovery exists to fix. + corrected = _normalize_value(str(cache_path)) + result = { + **result, + "nodes": [{**n, "source_file": corrected} for n in result["nodes"]], + "edges": [{**e, "source_file": corrected} for e in result["edges"]], + "hyperedges": [ + {**h, "source_file": corrected} for h in result["hyperedges"] + ], + } if allowed_paths is not None and cache_path not in allowed_paths: warnings.warn( "semantic cache skipped out-of-scope source_file " diff --git a/tests/test_semantic_cache_basename_recovery.py b/tests/test_semantic_cache_basename_recovery.py new file mode 100644 index 000000000..8aa204415 --- /dev/null +++ b/tests/test_semantic_cache_basename_recovery.py @@ -0,0 +1,30 @@ +"""#2973 — save_semantic_cache must recover a group whose reported +source_file never resolves to a real file, by an unambiguous basename match +against the already dispatched allowlist, instead of silently dropping it. + +A weak or local backend's adaptive-retry split path (llm.py bisecting a +chunk that overflowed context and retrying) sometimes re-prompts with a +reduced file subset and loses track of which of the original chunk's files +a given node came from, so its source_file drifts to something that never +resolves at all. Without recovery this silently discarded the group's nodes +and edges from the cache on every incremental run. +""" +from __future__ import annotations + +import pytest + +from graphify.cache import load_cached, save_semantic_cache + + +def test_malformed_but_basename_unique_path_recovers(tmp_path): + real = tmp_path / "sub" / "weird_named_file.py" + real.parent.mkdir(parents=True) + real.write_text("def f(): pass\n") + + nodes = [{"id": "n1", "label": "f", "source_file": "lost_dir/weird_named_file.py"}] + saved = save_semantic_cache(nodes, [], root=tmp_path, allowed_source_files=[real]) + assert saved == 1 + + cached = load_cached(real, root=tmp_path, kind="semantic") + assert cached is not None + assert {n["id"] for n in cached["nodes"]} == {"n1"}