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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017qfdzgbA5KedGEjD1AayNh
This commit is contained in:
ayushcodes10andClaude Sonnet 5 committed 2026-09-14 16:08:02 +05:30
1 parent 73f5a66370
commit 4b91dcd462
2 files changed
+48

No files matched your search

+18
View File
@@ -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 "
@@ -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"}