Skip to content
Closed
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
22 changes: 19 additions & 3 deletions graphify/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -2557,6 +2557,7 @@ def _parse_float(name: str, raw: str) -> float:
)

if not has_path:
detection = {}
code_files = []
doc_files = []
paper_files = []
Expand Down Expand Up @@ -2976,7 +2977,9 @@ def _progress(idx: int, total: int, _result: dict) -> None:
_live_hashes.add(_file_hash(_abs, target, cache_root=out_root))
except OSError:
pass
_prune_semantic_cache(out_root, _live_hashes)
# A pathless database extraction has no filesystem corpus to sweep.
if has_path:
_prune_semantic_cache(out_root, _live_hashes)
except Exception as exc:
print(f"[graphify extract] warning: could not prune semantic cache: {exc}", file=sys.stderr)
stages.mark("semantic extract")
Expand Down Expand Up @@ -3056,6 +3059,15 @@ def _progress(idx: int, total: int, _result: dict) -> None:
if has_path else None
)

def _invalidate_file_manifest_for_db_graph() -> None:
if has_path:
return
try:
manifest_path.unlink(missing_ok=True)
except OSError as exc:
print(f"error: could not invalidate file manifest: {exc}", file=sys.stderr)
sys.exit(1)

if no_cluster:
# --no-cluster: dump the raw merged extraction as graph.json.
# No NetworkX, no community detection, no analysis sidecar.
Expand Down Expand Up @@ -3140,6 +3152,7 @@ def _progress(idx: int, total: int, _result: dict) -> None:
)
sys.exit(1)
_backup(graphify_out)
_invalidate_file_manifest_for_db_graph()
from graphify.paths import write_json_atomic as _write_json_atomic
_write_json_atomic(graph_json_path, merged, indent=2)
stages.mark("write")
Expand All @@ -3159,7 +3172,8 @@ def _progress(idx: int, total: int, _result: dict) -> None:
f"est. cost: ${cost:.4f}"
)
try:
_save_manifest(_manifest_files, manifest_path=str(manifest_path), kind="both", root=target, scan_corpus=_scan_corpus, clear_semantic=_cleared_semantic)
if has_path:
_save_manifest(_manifest_files, manifest_path=str(manifest_path), kind="both", root=target, scan_corpus=_scan_corpus, clear_semantic=_cleared_semantic)
except Exception as exc:
print(f"[graphify extract] warning: could not write manifest: {exc}", file=sys.stderr)
if global_merge:
Expand Down Expand Up @@ -3231,6 +3245,7 @@ def _progress(idx: int, total: int, _result: dict) -> None:

from graphify.export import backup_if_protected as _backup
_backup(graphify_out)
_invalidate_file_manifest_for_db_graph()
# force=True bypasses the #479 shrink guard entirely. A full build
# legitimately shrinks (fuzzy dedup collapse, deleted code) so it keeps
# force=True — EXCEPT when this run's extraction was incomplete (an
Expand Down Expand Up @@ -3296,7 +3311,8 @@ def _progress(idx: int, total: int, _result: dict) -> None:
from graphify.paths import write_json_atomic as _wja
_wja(analysis_path, analysis, indent=2)
try:
_save_manifest(_manifest_files, manifest_path=str(manifest_path), kind="both", root=target, scan_corpus=_scan_corpus, clear_semantic=_cleared_semantic)
if has_path:
_save_manifest(_manifest_files, manifest_path=str(manifest_path), kind="both", root=target, scan_corpus=_scan_corpus, clear_semantic=_cleared_semantic)
except Exception as exc:
print(f"[graphify extract] warning: could not write manifest: {exc}", file=sys.stderr)

Expand Down
110 changes: 110 additions & 0 deletions tests/test_extract_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -789,6 +789,116 @@ def test_extract_timing_flag_emits_stage_timings(monkeypatch, tmp_path, capsys):
assert "graphify timing" not in capsys.readouterr().err


@pytest.mark.parametrize(
"postgres_args",
[["--postgres", "test-dsn"], ["--postgres=test-dsn"]],
)
@pytest.mark.parametrize("cluster_args", [[], ["--no-cluster"]])
def test_pathless_postgres_extract_initializes_empty_detection(
monkeypatch, tmp_path, postgres_args, cluster_args
):
calls = []

def _introspect(dsn):
calls.append(dsn)
return {
"nodes": [
{
"id": "postgresql_users",
"label": "users",
"type": "table",
"file_type": "code",
"source_file": "postgresql:/localhost/test",
}
],
"edges": [],
}

corpus = tmp_path / "corpus"
corpus.mkdir()
(corpus / "app.py").write_text("def app():\n return 1\n")
launcher = tmp_path / "launcher"
launcher.mkdir()
monkeypatch.chdir(launcher)
out_root = tmp_path / "output"
graph_path = out_root / "graphify-out" / "graph.json"
manifest = out_root / "graphify-out" / "manifest.json"
monkeypatch.setattr(mainmod, "_check_skill_version", lambda _: None)
monkeypatch.setattr("graphify.pg_introspect.introspect_postgres", _introspect)

def _run(argv):
monkeypatch.setattr(mainmod.sys, "argv", argv)
try:
mainmod.main()
except SystemExit as exc:
assert exc.code in (None, 0)

_run(
[
"graphify",
"extract",
str(corpus),
"--code-only",
"--no-cluster",
"--out",
str(out_root),
]
)
assert manifest.exists()
assert "app.py" in _node_sources(graph_path)
manifest_content = manifest.read_text()
(out_root / "graphify-out" / ".graphify_semantic_marker").write_text(
'{"output_tokens": 1}'
)

cache_entry = (
out_root
/ "graphify-out"
/ "cache"
/ "semantic"
/ "deadbeef.json"
)
cache_entry.parent.mkdir(parents=True)
cache_entry.write_text('{"nodes": [], "edges": []}')
_run(
[
"graphify",
"extract",
*postgres_args,
*cluster_args,
"--out",
str(out_root),
]
)
assert calls == ["test-dsn"]
assert cache_entry.exists()
assert not manifest.exists()
assert "postgresql:/localhost/test" in _node_sources(graph_path)
backups = [
path
for path in (out_root / "graphify-out").iterdir()
if path.is_dir() and (path / "manifest.json").exists()
]
assert backups
assert backups[0].joinpath("manifest.json").read_text() == manifest_content

_run(
[
"graphify",
"extract",
str(corpus),
"--code-only",
"--no-cluster",
"--out",
str(out_root),
]
)
final_sources = _node_sources(graph_path)
assert "app.py" in final_sources
assert "postgresql:/localhost/test" not in final_sources
assert manifest.exists()


# ---------------------------------------------------------------------------
# #1909: a newly-excluded file's nodes must be pruned from graph.json on the
# next incremental extract even when the manifest never listed the file (the
Expand Down
Loading