Skip to content
Merged
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
9 changes: 8 additions & 1 deletion code_review_graph/tools/query.py
Original file line number Diff line number Diff line change
Expand Up @@ -373,10 +373,17 @@ def add_result(result: dict[str, Any], edge: Any | None = None) -> None:
}

if not node and pattern not in ("consumers_of", "file_summary"):
return {
# This branch, not the empty-result path below, is where an
# unresolved target actually lands for most patterns, so the
# not-indexed marker has to be attached here too.
unresolved: dict[str, Any] = {
"status": "not_found",
"summary": f"No node found matching '{target}'.",
}
unresolved_note = empty_query_confidence(store, root, pattern, target, None)
if unresolved_note:
unresolved["confidence"] = unresolved_note
return unresolved

qn = node.qualified_name if node else target

Expand Down
21 changes: 21 additions & 0 deletions code_review_graph/uncertainty.py
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,19 @@ def not_indexed_note(target: str) -> str:
)


def unresolved_stale_note(target: str) -> str:
"""Say the target is missing from a graph that predates HEAD.

A stale graph explains the miss and has a remedy, so it outranks the
flat "not indexed" wording, which reads like a permanent limitation.
"""
return _interpolated_target(
"graph is stale: no node matching '",
target,
"'; the graph predates HEAD, so run `code-review-graph update` first",
)


def _confirmed_note(target: str, current: bool) -> str:
"""Say the zero is a real absence, so the agent can stop searching.

Expand Down Expand Up @@ -343,6 +356,14 @@ def empty_query_confidence(
"""
try:
if node is None:
if store.get_stats().total_nodes == 0:
return _bounded(
"graph is empty: nothing is indexed, so this 0 says "
"nothing about the code; run `code-review-graph build`"
)
stale, _unused = _staleness(store, root, None)
if stale:
return _bounded(unresolved_stale_note(target))
return _bounded(not_indexed_note(target))

stale, current = _staleness(store, root, getattr(node, "file_path", None))
Expand Down
87 changes: 87 additions & 0 deletions tests/test_uncertainty.py
Original file line number Diff line number Diff line change
Expand Up @@ -466,3 +466,90 @@ def test_direct_call_returns_none_on_failure(repo, monkeypatch):
assert empty_query_confidence(
store, repo, "callers_of", "whatever", None,
) is None


# ---------------------------------------------------------------------------
# The not_found branch
#
# For every pattern except consumers_of and file_summary, an unresolved target
# returns early with status "not_found" and never reaches the empty-result
# path. That branch is where an unindexed target actually lands in production,
# so the marker has to be attached there too.
# ---------------------------------------------------------------------------


@pytest.mark.parametrize(
"pattern",
["callers_of", "callees_of", "imports_of", "tests_for", "inheritors_of"],
)
def test_not_found_response_carries_a_marker(repo, pattern):
"""An unresolved target must never come back as a bare not_found."""
result = query_graph(
pattern=pattern, target="NoSuchSymbol", repo_root=str(repo),
)

assert result["status"] == "not_found"
assert result["confidence"]
assert "NoSuchSymbol" in result["confidence"]


def test_unresolved_target_on_a_stale_graph_says_stale_not_unindexed(repo, monkeypatch):
"""A stale graph explains the miss and has a remedy, so it must win.

Calling it "not indexed" reads like a permanent limitation and sends the
agent looking for a parser gap that is not there.
"""
with _store(repo) as store:
store.set_metadata("git_head_sha", "0" * 40)
store.commit()

monkeypatch.setattr(uncertainty, "_live_git_head", lambda root: "f" * 40)
result = query_graph(
pattern="callers_of", target="NoSuchSymbol", repo_root=str(repo),
)

assert "stale" in result["confidence"]
assert "update" in result["confidence"]
assert "not indexed" not in result["confidence"]


def test_unresolved_target_on_a_current_graph_says_not_indexed(repo, monkeypatch):
"""With currency established, not-indexed is the honest answer."""
with _store(repo) as store:
store.set_metadata("git_head_sha", "a" * 40)
store.commit()

monkeypatch.setattr(uncertainty, "_live_git_head", lambda root: "a" * 40)
result = query_graph(
pattern="callers_of", target="NoSuchSymbol", repo_root=str(repo),
)

assert "not indexed" in result["confidence"]
assert "stale" not in result["confidence"]


def test_unresolved_target_on_an_empty_graph_says_build(tmp_path):
"""Nothing indexed at all is a build problem, not a missing symbol."""
root = (tmp_path / "empty").resolve()
(root / ".code-review-graph").mkdir(parents=True)
(root / ".git").mkdir()
GraphStore(root / ".code-review-graph" / "graph.db").close()

result = query_graph(
pattern="callers_of", target="Anything", repo_root=str(root),
)

assert "graph is empty" in result["confidence"]
assert "build" in result["confidence"]


def test_resolved_target_with_results_still_has_no_marker(repo):
"""The token-budget guarantee holds on the not_found-adjacent path too."""
result = query_graph(
pattern="callers_of",
target=f"{(repo / 'auth.py').as_posix()}::login",
repo_root=str(repo),
)

assert result["result_count"] >= 1
assert "confidence" not in result