Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 29 additions & 1 deletion graphify/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,31 @@
)


UNCOVERED_FILES_NAME = ".graphify_uncovered_files.json"


def _write_uncovered_files(graphify_out: Path, files: "list[str]") -> "Path | None":
"""Record the files a semantic extraction dispatched but produced no nodes for.

The warning names five of them and counts the rest, and the full list was
computed and then dropped, so recovering it meant diffing the graph against
a filesystem walk (#3574). An empty list removes the file rather than
leaving one that describes an earlier run.

Returns the path written, or None. Never raises: this is a diagnostic, and
an output directory that refuses it must not cost the extraction.
"""
target = Path(graphify_out) / UNCOVERED_FILES_NAME
try:
if not files:
target.unlink(missing_ok=True)
return None
target.write_text(json.dumps(sorted(files), indent=2) + "\n", encoding="utf-8")
except OSError:
return None
return target


def _default_graph_path() -> str:
return str(Path(_GRAPHIFY_OUT) / "graph.json")

Expand Down Expand Up @@ -4000,14 +4025,17 @@ def _progress(idx: int, total: int, _result: dict) -> None:
# fraction of the graph, so it must arm the guard exactly like a
# crashed chunk does. --allow-partial still overrides.
_omitted_files = list(fresh.get("uncovered_files") or [])
_uncovered_path = _write_uncovered_files(graphify_out, _omitted_files)
if _omitted_files or _partial_semantic_files:
_extraction_incomplete = True
_where = f" The full list is in {_uncovered_path}." if _uncovered_path else ""
print(
f"[graphify extract] semantic extraction is incomplete: "
f"{len(_omitted_files)} dispatched file(s) produced no nodes and "
f"{len(_partial_semantic_files)} came back truncated or hollow. "
f"The shrink guard stays armed for this write; pass "
f"--allow-partial to overwrite a larger existing graph anyway.",
f"--allow-partial to overwrite a larger existing graph anyway."
f"{_where}",
file=sys.stderr,
)
try:
Expand Down
53 changes: 53 additions & 0 deletions tests/test_hollow_chunks_arm_shrink_guard.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@

import pytest

from pathlib import Path

import graphify.__main__ as mainmod


Expand Down Expand Up @@ -116,3 +118,54 @@ def _refuse(G, communities, output_path, *, force=False, **kwargs):
code = _run()
assert code not in (None, 0)
assert not (out_dir / "graphify-out" / "manifest.json").exists()


# --- the omitted files must be recoverable, not just counted (#3574) ---


def _uncovered_list(out_dir):
from graphify.cli import UNCOVERED_FILES_NAME

return out_dir / "graphify-out" / UNCOVERED_FILES_NAME


def test_the_omitted_files_are_written_where_a_user_can_read_them(monkeypatch, tmp_path, capsys):
"""The warning names five and counts the rest.

The full list was computed into ``uncovered_files`` and then read only for
its length, so recovering it meant diffing the graph against a filesystem
walk.
"""
import json

_record_force(monkeypatch)
out_dir = _arm(monkeypatch, tmp_path, uncovered=("GUIDE.md",))
_run()

listed = json.loads(_uncovered_list(out_dir).read_text(encoding="utf-8"))
assert [Path(p).name for p in listed] == ["GUIDE.md"]
assert str(_uncovered_list(out_dir)) in capsys.readouterr().err


def test_a_covered_run_clears_an_earlier_list(monkeypatch, tmp_path):
"""A leftover list describes files that are covered now, which is worse than none."""
_record_force(monkeypatch)
out_dir = _arm(monkeypatch, tmp_path)
stale = _uncovered_list(out_dir)
stale.parent.mkdir(parents=True, exist_ok=True)
stale.write_text('["plugin/gone.php"]', encoding="utf-8")

_run()

assert not stale.exists()


def test_the_list_never_costs_the_extraction(tmp_path, monkeypatch):
"""A directory that refuses the write loses the diagnostic, not the run."""
from graphify.cli import _write_uncovered_files

def _refuse(self, *args, **kwargs):
raise PermissionError(f"read-only: {self}")

monkeypatch.setattr(Path, "write_text", _refuse)
assert _write_uncovered_files(tmp_path, ["a.php"]) is None
Loading