diff --git a/graphify/cli.py b/graphify/cli.py index 35e397e60..daebe9343 100644 --- a/graphify/cli.py +++ b/graphify/cli.py @@ -2557,6 +2557,7 @@ def _parse_float(name: str, raw: str) -> float: ) if not has_path: + detection = {} code_files = [] doc_files = [] paper_files = [] @@ -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") @@ -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. @@ -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") @@ -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: @@ -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 @@ -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) diff --git a/tests/test_extract_cli.py b/tests/test_extract_cli.py index 4133002c0..b1c369a5e 100644 --- a/tests/test_extract_cli.py +++ b/tests/test_extract_cli.py @@ -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