From 4b91dcd462de21af61af6644ccbdb4bd8fd26cf1 Mon Sep 17 00:00:00 2001 From: ayushcodes10 Date: Mon, 14 Sep 2026 16:08:02 +0530 Subject: [PATCH] Correct recovered groups own source_file before writing them A recovered group's write KEY pointed at the right file, but every node, edge, and hyperedge inside it still carried the original unresolvable source_file string. load_cached rejects an entry on read if any item's source_file does not match the path it was loaded under, so the previous commit alone wrote a group that reported as saved but could never actually be read back, silently reproducing the exact loss this fix exists for. This corrects every item's source_file to the recovered path's portable form before the group is written, only when recovery actually redirected the key, so an unrecovered group stays byte identical to before. Includes the regression test proving the full round trip: a group recovered by basename now survives a subsequent load_cached call. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_017qfdzgbA5KedGEjD1AayNh --- graphify/cache.py | 18 +++++++++++ .../test_semantic_cache_basename_recovery.py | 30 +++++++++++++++++++ 2 files changed, 48 insertions(+) create mode 100644 tests/test_semantic_cache_basename_recovery.py 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"}