Skip to content

fix(cache): the atexit stat-index flush must not resurrect a deleted corpus (#2974) - #3107

Open
abhay-codes07 wants to merge 1 commit into
Graphify-Labs:v8from
abhay-codes07:fix/stat-index-husk
Open

fix(cache): the atexit stat-index flush must not resurrect a deleted corpus (#2974)#3107
abhay-codes07 wants to merge 1 commit into
Graphify-Labs:v8from
abhay-codes07:fix/stat-index-husk

Conversation

@abhay-codes07

Copy link
Copy Markdown
Contributor

Closes #2974.

The problem

_flush_stat_index runs at atexit and did p.parent.mkdir(parents=True, exist_ok=True) before writing graphify-out/cache/stat-index.json. If the analyzed directory is deleted while graphify is running, the exit-time flush recreates the whole path and leaves a husk containing nothing but the index.

The trigger is ordinary: a post-commit hook runs graphify update . >/dev/null 2>&1 & in a short-lived git worktree; the branch merges and git worktree remove deletes the tree while the rebuild is still going; the rebuild finishes and resurrects the dead path. The reporter found 81 such <repo>-<branch> directories over a few weeks.

The change

The index is a pure optimisation, so when its root no longer exists it is simply not written — and the dirty flag is cleared so nothing retries. A root that still exists keeps the old behaviour exactly: a first run writes the index before graphify-out/ exists at all, and three existing test_stat_index_portability tests pin that, so the check is deliberately on the root, not on the output directory.

The issue's own deterministic repro (file_hashrmtree_flush_stat_index()) now prints "ok".

Tests

tests/test_stat_index_husk.py — 3 tests: a corpus deleted mid-run stays deleted (and nothing is left pending), a redirected cache_root that vanished is not recreated, and a live run still writes the index into a not-yet-existing graphify-out/. With the fix reverted, 2 of 3 fail. test_stat_index_portability, test_cache, test_word_count_cache, test_extract_cache_location, test_cli_export and test_incremental are unchanged; the full suite matches the v8 baseline.

…corpus (Graphify-Labs#2974)

A post-commit hook runs `graphify update . &` in a short-lived worktree;
the branch merges and `git worktree remove` deletes the tree while the
rebuild is still running. _flush_stat_index then did `mkdir -p` on the
dead path and wrote graphify-out/cache/stat-index.json into it — a husk
directory per worktree, 81 of them over a few weeks for the reporter.

The index is a pure optimisation: when its root no longer exists it is
simply not written (and the dirty flag is cleared so nothing retries). A
root that still exists keeps the old behaviour — a first run writes the
index before graphify-out/ exists, and the existing tests pin that.
Copilot AI lite review requested due to automatic review settings August 26, 2026 11:24

Copilot AI 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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@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 4 advisory finding(s) below merit a look before merge.

Formal verification. 1 change(s) tested, no difference found (not proven).


Graphify review — findings

Guards the atexit stat-index flush in _flush_stat_index so it skips writing (and clears the dirty flag) when _stat_index_root no longer exists, instead of mkdir -p'ing the dead path back into a husk that holds only the index — fixing the case where graphify update . & outlives a git worktree remove (#2974). A live root still gets graphify-out/cache/ created on a first run as before.

Worth a look

  • _flush_stat_index modifies module globals without declaring them globalgraphify/cache.py:388 · Escalate · high
    • agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
  • _flush_stat_index references _stat_index_root without global declaration when assigning _stat_index_dirtygraphify/cache.py:388 · Escalate · high
    • agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
  • _flush_stat_index checks corpus root instead of cache output parentgraphify/cache.py:388 · Escalate · medium
    • agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
  • Stat-index flush still has a TOCTOU path that can recreate a deleted corpusgraphify/cache.py:389 · 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 — 1352 functions depend on the 90 functions this change touches.

Health — this change adds coupling hotspots:

  • new: extract() — 498 callers, 42 callees
  • new: _rebuild_code() — 98 callers, 50 callees
  • new: detect() — 108 callers, 15 callees
  • new: save_semantic_cache() — 58 callers, 9 callees
  • new: load_cached() — 48 callers, 7 callees
  • new: file_hash() — 50 callers, 6 callees
  • new: extract_corpus_parallel() — 26 callers, 11 callees
  • new: dispatch_command() — 2 callers, 122 callees
  • …and 18 more — each is listed as a finding

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

Formal verification

No difference found (not proven): No behavior difference found in \_flush\_stat\_index (not a proof).

The verifier ran both versions of \_flush\_stat\_index on many inputs and saw identical behavior every time. Strong evidence the change is safe, but evidence, not a proof.

Guarantee: Empirical: differential testing (both versions run on many generated inputs). A divergence on an untested input remains possible, so this is 'no counterexample found', not 'proven equivalent'.

Note: An input the sampler did not try could still differ.

· 26 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

Development

Successfully merging this pull request may close these issues.

atexit stat-index flush recreates the analyzed directory if it was deleted mid-run (worktree race leaves husk dirs)

2 participants