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
24 changes: 20 additions & 4 deletions graphify/llm.py
Original file line number Diff line number Diff line change
Expand Up @@ -2629,6 +2629,7 @@ def extract_corpus_parallel(
"input_tokens": 0, "output_tokens": 0,
"failed_chunks": 0, # count of chunks that raised — loud failure on chunk errors
}
failed_files: set[Path] = set()
total = len(chunks)

def _run_one(idx: int, chunk: list[Path]) -> tuple[int, dict | None, Exception | None]:
Expand Down Expand Up @@ -2711,6 +2712,7 @@ def _checkpoint_chunk(result: dict, chunk: "list[Path | FileSlice]") -> None:
if exc is not None:
print(f"[graphify] chunk {idx + 1}/{total} failed: {exc}", file=sys.stderr)
merged["failed_chunks"] += 1
failed_files.update(unit_path(item) for item in chunk)
continue
assert result is not None
_merge_into(merged, result)
Expand All @@ -2736,6 +2738,7 @@ def _checkpoint_chunk(result: dict, chunk: "list[Path | FileSlice]") -> None:
file=sys.stderr,
)
merged["failed_chunks"] += 1
failed_files.update(unit_path(item) for item in chunks[idx])
continue
assert result is not None
results_by_idx[idx] = result
Expand Down Expand Up @@ -2838,11 +2841,24 @@ def _out_of_scope(item: dict) -> bool:
if p.resolve() not in {c.resolve() for c in covered}
)
merged["uncovered_files"] = [str(p) for p in uncovered]
if uncovered:
shown = ", ".join(p.name for p in uncovered[:5])
more = f" (+{len(uncovered) - 5} more)" if len(uncovered) > 5 else ""
failed_resolved = {_resolve_against_root(p) for p in failed_files}
failed_uncovered = [p for p in uncovered if _resolve_against_root(p) in failed_resolved]
omitted_uncovered = [p for p in uncovered if _resolve_against_root(p) not in failed_resolved]
if failed_uncovered:
shown = ", ".join(p.name for p in failed_uncovered[:5])
more = f" (+{len(failed_uncovered) - 5} more)" if len(failed_uncovered) > 5 else ""
print(
f"[graphify] WARNING: {len(uncovered)}/{len(dispatched)} dispatched file(s) "
f"[graphify] WARNING: {len(failed_uncovered)}/{len(dispatched)} dispatched "
f"file(s) produced no nodes because their semantic chunk failed before "
f"returning a usable result: {shown}{more}. See chunk errors above; a re-run "
"will retry them.",
file=sys.stderr,
)
if omitted_uncovered:
shown = ", ".join(p.name for p in omitted_uncovered[:5])
more = f" (+{len(omitted_uncovered) - 5} more)" if len(omitted_uncovered) > 5 else ""
print(
f"[graphify] WARNING: {len(omitted_uncovered)}/{len(dispatched)} dispatched file(s) "
f"produced no nodes and are absent from the graph: {shown}{more}. The model "
"returned a response but omitted them; a re-run will retry them.",
file=sys.stderr,
Expand Down
29 changes: 29 additions & 0 deletions tests/test_chunking.py
Original file line number Diff line number Diff line change
Expand Up @@ -279,6 +279,34 @@ def maybe_fail(chunk, **kwargs):
assert "failed" in err and "simulated API error" in err


def test_failed_chunk_is_not_reported_as_successful_model_omission(tmp_path, capsys):
"""A dependency or backend exception means no model response exists.

The reconciliation warning must not claim the model returned a response and
omitted the file when its chunk failed before producing any result.
"""
from graphify.llm import extract_corpus_parallel

doc = tmp_path / "guide.md"
doc.write_text("# Guide\n", encoding="utf-8")

with patch(
"graphify.llm.extract_files_direct",
side_effect=ImportError(
"OpenAI package not installed. Install with: pip install 'graphifyy[gemini]'"
),
):
result = extract_corpus_parallel(
[doc], backend="gemini", root=tmp_path,
token_budget=None, chunk_size=1, max_concurrency=1,
)

assert result["failed_chunks"] == 1
err = capsys.readouterr().err
assert "failed before returning a usable result" in err
assert "returned a response but omitted" not in err


def test_checkpoint_scopes_cache_writes_to_chunk_files(tmp_path):
"""#1757: the per-chunk incremental checkpoint must not let a chunk's
mis-attributed node clobber another corpus file's semantic cache. A chunk
Expand Down Expand Up @@ -424,6 +452,7 @@ def omit_odd(chunk, **kwargs):
assert uncovered == {"doc1.md", "doc3.md"}, f"reconciliation missed omissions: {uncovered}"
err = capsys.readouterr().err
assert "produced no nodes" in err and "doc1.md" in err
assert "returned a response but omitted" in err


def test_out_of_scope_nodes_are_dropped_from_merged_result(tmp_path, capsys):
Expand Down