Skip to content

fix(cache): recover semantic cache groups with malformed source_file by basename - #3553

Open
ayushcodes10 wants to merge 7 commits into
Graphify-Labs:v8from
ayushcodes10:fix-2973-semantic-cache-basename-recovery
Open

ayushcodes10 wants to merge 7 commits into
Graphify-Labs:v8from
ayushcodes10:fix-2973-semantic-cache-basename-recovery

Conversation

@ayushcodes10

Copy link
Copy Markdown
Contributor

Fixes #2973.

Summary

save_semantic_cache groups nodes/edges/hyperedges by source_file, then silently skips any group whose path does not resolve to a real file. The reporter found that during a long incremental run against a weak or local backend, the adaptive-retry split path (llm.py bisecting a chunk that overflowed the model's context) sometimes reports a source_file for a sub-chunk that never resolves at all, so that group's nodes and edges vanish from the cache on every run — an "incremental" extraction kept re-processing the same already-extracted chunks.

This recovers such a group by an unambiguous basename match against allowed_source_files (the known-good, already-dispatched file list already threaded through from _checkpoint_chunk), matching the fix the reporter tested and verified locally. Two things had to change for it to actually work end to end:

  • The write key for a recovered group is redirected to the real file (_recover_group_path, used consistently by both the write loop and group_skipped's dangling-reference pruning, so the two never disagree on whether a group was written).
  • Each item inside a recovered group also has its own source_file field corrected to match. Without this, load_cached's _semantic_entry_matches_path integrity check rejects the entry on the very next read (it verifies every item's source_file matches the path being loaded), so a naive fix would report the group as "saved" while producing an entry that can never actually be read back — silently reproducing the same loss it's meant to fix. This was caught by writing a direct save-then-load round-trip test rather than only asserting the saved count.

An ambiguous basename (two allowed files sharing a name) or a call with no allowed_source_files at all still falls through to the exact previous skip behavior, so unscoped callers are byte-identical.

Changes

  • graphify/cache.py: add _recover_group_path, wire it into group_skipped and the write loop, and correct a recovered group's own source_file fields before writing.
  • tests/test_semantic_cache_basename_recovery.py (new): basename recovery + save/load round trip, ambiguous basename stays skipped, unscoped calls unaffected, dangling-edge pruning stays consistent for a recovered group.
  • CHANGELOG.md: entry under 0.9.61 (unreleased).

Testing

  • python -m pytest tests/test_semantic_cache_basename_recovery.py -q — 5 passed
  • Full suite: python -m pytest -q — 5489 passed, 68 skipped (pre-existing unrelated failures excluded: tests/test_ollama_retry_cap.py missing the optional openai module, and test_ts_normalizer_scales_linearly_on_large_files / test_hyperedge_convex_hull_js_is_geometrically_sound, both known environment-specific flakes unrelated to this change)
  • python -m tools.skillgen --check — OK

🤖 Generated with Claude Code

https://claude.ai/code/session_017qfdzgbA5KedGEjD1AayNh

ayushcodes10 and others added 7 commits September 14, 2026 16:02
save_semantic_cache groups nodes and edges by source_file, then skips
any group whose path does not resolve to a real file. A weak or local
backend's adaptive retry split path sometimes reports a source_file
for a group that never resolves at all, silently dropping that
group's nodes and edges from the cache on every incremental run.

This adds a helper that recovers such a group by an unambiguous
basename match against the known good, already dispatched allowlist,
with no behavior change yet since nothing calls it. The write loop is
switched over in the next commit.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017qfdzgbA5KedGEjD1AayNh
Fixes issue Graphify-Labs#2973.

group_skipped and the save_semantic_cache write loop now call
_recover_group_path instead of resolving fpath directly, so a group
whose reported source_file never resolves to a real file gets one more
chance: an unambiguous basename match against the already dispatched
allowlist. An ambiguous or genuinely bogus basename still falls
through to the previous skip behavior unchanged, and unscoped callers
(no allowed_source_files) are byte identical to before since recovery
only runs when an allowlist is available.

This stops an incremental run from silently redoing already processed
chunks every time the adaptive retry split path reports a source_file
that a weak or local backend lost track of.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017qfdzgbA5KedGEjD1AayNh
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
Two allowed files sharing a basename must not let recovery guess
between them: an unresolvable source_file with an ambiguous basename
still falls through to the previous skip behavior unchanged.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017qfdzgbA5KedGEjD1AayNh
Confirms recovery never runs when allowed_source_files is omitted: a
genuinely bogus path is skipped exactly as before this fix, and a
normal well formed path in the same batch still saves and loads back.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017qfdzgbA5KedGEjD1AayNh
group_skipped feeds the dangling reference pruning pass and the write
loop must agree with it on whether a recovered group is written or
skipped. This proves an edge between two nodes inside the same
recovered group survives instead of being pruned as referencing a
supposedly skipped id.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017qfdzgbA5KedGEjD1AayNh
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017qfdzgbA5KedGEjD1AayNh

@graphify-labs graphify-labs Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Graphify reviewed this change.

Worth a look — the grounded gate found no coupling regressions or blocking issues, but 1 advisory finding(s) below merit a look before merge.

Formal verification. No changes could be formally verified in this run.


Graphify review — findings

Recovers a semantic-cache group whose reported source_file never resolves to a real file by matching its basename against the already-dispatched allowlist, so nodes/edges from the adaptive-retry split path reach the cache instead of being silently dropped and re-processed on every incremental run. _recover_group_path performs the match only when the basename is unambiguous in allowed_paths, and group_skipped uses it too so a recovered group's intra-group edges aren't pruned as dangling; when recovery redirects the write key, every item's source_file is rewritten to the corrected path so the entry can actually be read back. An ambiguous or genuinely bogus basename, or any call without an allowlist, still falls through to the existing skip-and-warn behaviour.

Worth a look

  • Recovered write key can collide when two unresolvable groups share a basenamegraphify/cache.py:1593 · Escalate · medium
    • agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
Analysis details — impact, health, verification

Impact & health

Graphify review

Impact — 1773 functions depend on the 296 functions this change touches.

Health — this change adds coupling hotspots:

  • new: extract() — 589 callers, 44 callees
  • new: _rebuild_code() — 116 callers, 51 callees
  • new: detect() — 112 callers, 15 callees
  • new: save_semantic_cache() — 63 callers, 9 callees
  • new: load_cached() — 53 callers, 7 callees
  • new: file_hash() — 51 callers, 6 callees
  • new: extract_corpus_parallel() — 26 callers, 11 callees
  • new: dispatch_command() — 2 callers, 124 callees
  • …and 19 more — each is listed as a finding

Verification — 1773 functions in the blast radius were not formally verified this run (proofs are advisory here).

Gate & verification

graphify gate

PASS — objectively clean (no health regressions, tests not run — proofs not run this pass (advisory)). Grounded, not self-assessed.

Advisory (not blocking):

  • verification_scope: 1061 function(s) in the blast radius were not formally verified this run

Test selection

Test selection

276 of 276 test file(s) selected (100%) via static blast radius.

Escalated to a full run for safety — the selection is not trustworthy on its own (see below). CI should run the whole suite.

  • tests/test_affected_cli.py — full-run-safety
  • tests/test_affected_member_seed.py — full-run-safety
  • tests/test_agents_platform.py — full-run-safety
  • tests/test_analyze.py — full-run-safety
  • tests/test_anthropic_custom_endpoint.py — full-run-safety
  • tests/test_antigravity_install.py — full-run-safety
  • tests/test_apm_fallback_version.py — full-run-safety
  • tests/test_architecture_doc.py — full-run-safety
  • tests/test_astro_extraction.py — full-run-safety
  • tests/test_astro_import_ids.py — full-run-safety
  • tests/test_atomic_canvas_export.py — full-run-safety
  • tests/test_atomic_version_stamp.py — full-run-safety
  • tests/test_atomic_writes.py — full-run-safety
  • tests/test_backend_env_isolation.py — full-run-safety
  • tests/test_backend_extras.py — full-run-safety
  • tests/test_benchmark.py — full-run-safety
  • tests/test_benchmark_raw_graph.py — full-run-safety
  • tests/test_build.py — full-run-safety
  • tests/test_build_merge_dedup_scope.py — full-run-safety
  • tests/test_build_merge_hyperedges_and_prune.py — full-run-safety
  • tests/test_build_merge_shrink_guard.py — full-run-safety
  • tests/test_builtin_global_type_refs.py — full-run-safety
  • tests/test_cache.py — impact, full-run-safety
  • tests/test_callflow_html.py — full-run-safety
  • tests/test_cargo_introspect.py — full-run-safety
  • tests/test_carried_hyperedge_remap.py — full-run-safety
  • tests/test_case_sensitive_resolution.py — full-run-safety
  • tests/test_charmap_encoding.py — impact, full-run-safety
  • tests/test_chunking.py — impact, full-run-safety
  • tests/test_cjs_module_extension.py — full-run-safety
  • tests/test_claude_cli_backend.py — full-run-safety
  • tests/test_claude_md.py — full-run-safety
  • tests/test_cli_broken_pipe.py — full-run-safety
  • tests/test_cli_export.py — full-run-safety
  • tests/test_cli_help.py — full-run-safety
  • tests/test_cluster.py — full-run-safety
  • tests/test_codebuddy.py — full-run-safety
  • tests/test_community_hub_labels.py — full-run-safety
  • tests/test_community_labels_skill.py — full-run-safety
  • tests/test_confidence.py — full-run-safety
  • tests/test_corrupt_graph_json.py — full-run-safety
  • tests/test_cpp_nested_and_cli.py — full-run-safety
  • tests/test_cpp_objc_cross_file_calls.py — full-run-safety
  • tests/test_cpp_preprocess.py — full-run-safety
  • tests/test_cross_extension_reexport_self_cycle.py — full-run-safety
  • tests/test_cross_language_call_resolution.py — full-run-safety
  • tests/test_cross_repo_member_calls.py — full-run-safety
  • tests/test_cross_repo_shared_types.py — full-run-safety
  • tests/test_csharp_call_site_generic_args.py — full-run-safety
  • tests/test_csharp_enum_members.py — full-run-safety
  • … and 226 more

non-code file(s) changed (CHANGELOG.md) → running the full suite for safety (a code graph can't see config/fixture/data deps)

changed code file(s) with no mapped test (CHANGELOG.md) — a coverage gap or a missing link — running the full suite rather than only the selected tests

Selection is safe under the controlled-regression assumption; always-run tests + a periodic full run are the backstops. Advisory — it never changes the check verdict.

Formal verification

Could not verify: Could not verify save\_semantic\_cache.

The verifier did not have enough to check save\_semantic\_cache, so it is saying so rather than guessing. No false assurance is the whole point.

Guarantee: No guarantee either way, this is an honest abstention, not a pass.

Note: Reason: parameter `root` is annotated `Path` — outside the synthesizable primitive/collection set

· 27 more finding(s) on lines outside this diff (see the check run).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant