Skip to content

fix(extract): scope fresh semantic results to dispatched files (#2926) - #2928

Open
SinghAman21 wants to merge 2 commits into
Graphify-Labs:v8from
SinghAman21:fix/2926-scope-fresh-extraction-to-dispatched-files
Open

fix(extract): scope fresh semantic results to dispatched files (#2926)#2928
SinghAman21 wants to merge 2 commits into
Graphify-Labs:v8from
SinghAman21:fix/2926-scope-fresh-extraction-to-dispatched-files

Conversation

@SinghAman21

Copy link
Copy Markdown
Contributor

Closes #2926

Summary

During incremental extraction, LLM output whose source_file pointed at a file
that was not dispatched silently deleted that file's entire prior
contribution from graph.json. The semantic cache has been guarded against this
since #1757 (allowed_source_files); the graph build was not.

Root cause

On an incremental run only the changed subset is dispatched, and unchanged files
are never read back from the semantic cache — their graph contribution survives
precisely because they are absent from the new chunks. A stray fragment the
model attributes to such a file puts it back into the replace-set:

  1. _save_semantic_cache(..., allowed_source_files=uncached_paths) correctly
    discards the strays from the cache (skipped out-of-scope source_file),
  2. but the same unfiltered fresh result was extended into sem_result,
  3. so build_merge() saw the file among new_chunks, dropped its prior
    nodes/edges per tier, and merged in only the fragment,
  4. the manifest still said unchanged → never re-dispatched, never repaired.
    Loss was permanent until a full rebuild.

Strays attributed to nonexistent paths (src/foo.ts) became phantom nodes no
delete pass ever matched.

Fix

  • cache.py — new scope_semantic_result(): filters a result dict in place
    to the dispatched-file allowlist, using the exact path normalization as
    save_semantic_cache's guard (so an item one keeps, the other can't skip).
    Mirrors fix(cache): prune edges/hyperedges referencing never-written node groups #1916 by also dropping surviving edges/hyperedges that reference a
    dropped node id — except duplicate-attribution ids, which stay reference-safe.
  • cli.py: applies the filter to fresh immediately after extraction,
    before both the cache save and the sem_result extension. This also closes a
    residual hole where strays from chunk A attributed to a file dispatched in
    chunk B polluted B's cache entry at the final save. Drops are logged:
    [graphify extract] dropped N out-of-scope item(s) attributed to M file(s) ...

Testing

  • 4 unit tests for scope_semantic_result: group dropping (incl. nonexistent
    paths), absolute/relative identity equivalence, dangling-edge pruning with
    duplicate-attribution protection, unscoped no-op.
  • End-to-end CLI regression test reproducing the issue: seed run gives
    OTHER.md a two-node contribution; next run changes only README.md and the
    stubbed model answers with README's node plus strays for OTHER.md and
    src/foo.ts. Asserts OTHER.md's original nodes survive, no stray/phantom
    enters the graph, and the drop is reported. Verified red without the fix,
    green with it.
  • Full suite: 3281 passed (only pre-existing, unrelated test_ollama_retry_cap
    failure, present on the clean tree).

@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.

Looks safe to merge — no coupling regressions and no blocking issues, checked against the code graph (not a self-assessment).

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


Graphify review — findings

Adds scope_semantic_result in graphify/cache.py, a graph-side allowlist filter that drops nodes/edges/hyperedges attributed to files not dispatched this run (and prunes edges/hyperedges dangling to dropped node ids, sparing duplicate-attribution ids). Wires it into dispatch_command in cli.py to scope the fresh extraction result against uncached_paths before it reaches build_merge, with a printed summary of dropped items, mirroring the existing save_semantic_cache write-guard. Covers the new function and CLI path with tests in test_cache.py and test_extract_cli.py.

No blocking issues surfaced. 8 lower-confidence candidates did not survive cross-model review.

Analysis details — impact, health, verification

Impact & health

Graphify review

Impact — 1471 functions depend on the 384 functions this change touches.

Health — this change adds coupling hotspots:

  • new: extract() — 474 callers, 42 callees
  • new: _rebuild_code() — 98 callers, 50 callees
  • new: detect() — 108 callers, 15 callees
  • new: save_semantic_cache() — 55 callers, 11 callees
  • new: load_cached() — 43 callers, 7 callees
  • new: extract_corpus_parallel() — 26 callers, 11 callees
  • new: file_hash() — 44 callers, 6 callees
  • new: dispatch_command() — 2 callers, 120 callees
  • …and 20 more — each is listed as a finding

Verification — 1471 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: 1155 function(s) in the blast radius were not formally verified this run

Formal verification

Could not verify: Could not verify dispatch\_command.

The verifier did not have enough to check dispatch\_command, 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: not verifiable: all 23 sampled inputs raised on both versions — the function never executed, so 'no divergence' would be vacuous (mostly SystemExit — names the real obstacle, not a sampling gap)

· 1 grounded finding(s) anchored inline below; 27 more finding(s) on lines outside this diff (see the check run).

Comment thread graphify/cache.py

@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.

Looks safe to merge — no coupling regressions and no blocking issues, checked against the code graph (not a self-assessment).

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


Graphify review — findings

Adds scope_semantic_result to graphify/cache.py, a graph-side scope filter mirroring save_semantic_cache's allowed_source_files write guard, and factors the shared path-matching into a new _semantic_source_matcher helper used by both. Wires it into dispatch_command in cli.py to drop (and log) fresh nodes/edges/hyperedges attributed to files not dispatched this run before they reach build_merge, plus dropping edges/hyperedges that dangle to pruned node ids. Covers the new behavior with tests in tests/test_cache.py and tests/test_extract_cli.py.

No blocking issues surfaced. 10 lower-confidence candidates did not survive cross-model review.

Analysis details — impact, health, verification

Impact & health

Graphify review

Impact — 1473 functions depend on the 386 functions this change touches.

Health — this change adds coupling hotspots:

  • new: extract() — 474 callers, 42 callees
  • new: _rebuild_code() — 98 callers, 50 callees
  • new: detect() — 108 callers, 15 callees
  • new: save_semantic_cache() — 55 callers, 9 callees
  • new: load_cached() — 43 callers, 7 callees
  • new: extract_corpus_parallel() — 26 callers, 11 callees
  • new: file_hash() — 44 callers, 6 callees
  • new: dispatch_command() — 2 callers, 120 callees
  • …and 20 more — each is listed as a finding

Verification — 1473 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: 1157 function(s) in the blast radius were not formally verified this run

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

Could not verify: Could not verify dispatch\_command.

The verifier did not have enough to check dispatch\_command, 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: not verifiable: all 23 sampled inputs raised on both versions — the function never executed, so 'no divergence' would be vacuous (mostly SystemExit — names the real obstacle, not a sampling gap)

· 2 grounded finding(s) anchored inline below; 26 more finding(s) on lines outside this diff (see the check run).

Comment thread graphify/cache.py
return source_identity, normalize_value


def save_semantic_cache(

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Health regressionsave_semantic_cache()

fans out to 9 callees (efferent coupling); 55 callers depend on it (afferent coupling).

Grounded coupling-delta finding (deterministic), not an LLM guess.

Comment thread graphify/cache.py
return saved


def scope_semantic_result(

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Health regressionscope_semantic_result()

7 callers depend on it (afferent coupling).

Grounded coupling-delta finding (deterministic), not an LLM guess.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Incremental merge: stray output misattributed to non-dispatched files silently deletes their prior graph contributions

1 participant