Skip to content

fix(watch): let the shrink guard refuse on a full rebuild - #3582

Open
L4XB wants to merge 1 commit into
Graphify-Labs:v8from
L4XB:fix/3579-shrink-guard-full-rebuild
Open

L4XB wants to merge 1 commit into
Graphify-Labs:v8from
L4XB:fix/3579-shrink-guard-full-rebuild

Conversation

@L4XB

@L4XB L4XB commented Sep 15, 2026

Copy link
Copy Markdown
Contributor

Closes #3579.

_check_shrink cannot refuse a write on a full rebuild. rebuilt_sources is the whole corpus there:

if changed_paths is None:
    rebuilt_sources = {... for p in code_files if p.is_relative_to(project_root)}

and _accounted treats membership as evidence that a loss was intended, so all(_accounted(n) for n in lost) is true for every node that has a source_file and for every node that has none. The guard is structurally absent on exactly the path its own refusal message recommends: "Run a full rebuild to be safe."

Observable on v8 with two files and no fixtures — a full rebuild that empties a.py goes 5 nodes to 3 and writes without a word:

with symbols  : total=5 {'a.py': 3, 'b.py': 2}
a.py emptied  : total=3 {'b.py': 2, 'a.py': 1}

The change

Membership says a file was scanned. It does not say that losing its nodes was intended. A re-extracted source now accounts for its own lost nodes only while it still contributes something to the new graph, with two exemptions kept intact:

  • Deleted files. Contributing nothing is the correct outcome, so deleted_sources (the set the call sites already fold into rebuilt_sources) is passed separately and exempted.
  • failed_sources. Unchanged, and still takes precedence: extraction did not complete, so the disappearance is the silent shrink.

The "removed every symbol" case does not regress, and that is measured rather than assumed: graphify keeps a file node for an emptied-but-present file (a.py: 1 above), so such a source stays in the surviving set and the write is still allowed without --force.

Test

Six cells in tests/test_watch.py, beside the existing guard cells. The one that names the defect fails on v8:

>   assert ok is False
E   assert True is False
FAILED test_check_shrink_refuses_a_source_that_produced_nothing_on_a_full_rebuild

The other five pin the boundaries: a symbol removed from a rebuilt source still passes (#1116), a deleted source still passes, a failed source still refuses, a node with no source_file is still accounted, and a loss from a source that was not rebuilt still refuses (the incremental case this guard was written for, which had no cell of its own — a mutation that dropped the membership check survived until it was added).

tests/test_watch.py and tests/test_build_merge_shrink_guard.py are green at 166 passed, 3 skipped. The full tests/ run has 26 failures, and the identical 26 fail on an unmodified v8 at fe66389 here — the two sets compare equal, so none of them are this change.

Mutation-checked: five seeded defects, including restoring the original _accounted, dropping the deleted-file exemption, and dropping the failed_sources precedence. Each is caught by a named cell; none survive.

On the sibling report: this is the guard half of what #3580 describes, so a rebuild that zeroes files would now refuse rather than write silently. It does not address why those files were zeroed — I could not reproduce that part on v8 and have posted the harness and the negative result there.

On a full rebuild `rebuilt_sources` is every file in the corpus, so
`_accounted` returned True for every lost node and `_check_shrink` could never
refuse. The protection was structurally absent on exactly the path its own
refusal message recommends as the remedy.

Membership in `rebuilt_sources` says a file was scanned, not that losing its
nodes was intended. A re-extracted source now accounts for its own losses only
while it still contributes something to the new graph: a source that goes from
N nodes to none while still on disk is the silent shrink this guard exists for.
Files the caller declared deleted stay exempt, since contributing nothing is
the correct outcome for them.

An emptied-but-present file keeps its file node, so a legitimate "removed every
symbol" edit still passes without --force.
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.

_check_shrink cannot fire on a full rebuild: rebuilt_sources is the entire corpus

1 participant