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
26 changes: 22 additions & 4 deletions graphify/serve.py
Original file line number Diff line number Diff line change
Expand Up @@ -1087,11 +1087,29 @@ def _adj(n):
# lost. Announcing "showing N of N nodes … among the 0 cut nodes" then
# reads as a false truncation warning that teaches an agent to distrust a
# complete answer and burn follow-up narrowing calls for nodes that were
# never cut (#2601). When every node is shown the answer is complete:
# return the full output with no banner rather than silently dropping
# edges under a misleading notice.
# never cut (#2601). When every node is shown the answer is complete, so
# edges are never dropped either (returning output[:cut_at] here would
# silently truncate them) — but that completeness guarantee is exactly
# why a query can quietly cost 4-6x its requested budget once the last
# node crosses the fit line (#2784): the check above only ever compared
# the FULL output (nodes+edges) against char_budget, so this branch was
# already known to be over budget, yet said nothing about it. Report the
# real size instead of silence — still the complete, non-truncated
# answer, just an honest one.
if cut_count == 0:
return output
if len(output) <= char_budget:
return output
total_edges = sum(1 for l in lines if l.startswith("EDGE "))
est_tokens = len(output) // 3
return (
f"[i] Complete answer over budget: all {total_nodes} nodes and "
f"{total_edges} edges shown (~{est_tokens} tokens vs the "
f"requested ~{token_budget}-token budget). Edges are never "
f"dropped once every node fits, so this is already the full "
f"answer — raising --budget further will not shrink it. Narrow "
f"with context_filter=['call'] or use get_node for a specific "
f"symbol to reduce size instead.\n\n"
) + output
# Prominent notice at the TOP so a truncated answer can never be mistaken
# for a complete one — silence used to read as absence (#BUG2). The
# notice + end marker sit OUTSIDE char_budget by design (two bounded
Expand Down
45 changes: 45 additions & 0 deletions tests/test_serve.py
Original file line number Diff line number Diff line change
Expand Up @@ -1523,6 +1523,51 @@ def test_subgraph_to_text_no_banner_when_only_edges_overflow():
assert len(edge_lines) == len(edges), "all edges must survive a complete answer"


def test_subgraph_to_text_overshoot_notice_when_edges_exceed_budget():
"""#2784: once every node fits, edges are never dropped (#2601) — but that
used to mean the char_budget check silently stopped applying, so a query
could cost 4-6x its requested budget with zero indication. The complete
answer must still be returned whole, but the overshoot must be visible and
must not repeat the "raise the budget" advice that caused the blow-up."""
import itertools

G = nx.Graph()
labels = [f"n{i}" for i in range(20)]
for lbl in labels:
G.add_node(lbl, label=lbl, source_file="f.py", source_location="L1", community="c")
edges = list(itertools.combinations(labels, 2))
for u, v in edges:
G.add_edge(u, v, relation="calls", confidence="high")
node_chars = len("\n".join(f"NODE {l} [src=f.py loc=L1 community=c]" for l in labels))
budget = (node_chars // 3) + 5 # fits every node, nowhere near every edge
text = _subgraph_to_text(G, set(G.nodes), edges, token_budget=budget)
node_lines = [l for l in text.splitlines() if l.startswith("NODE ")]
edge_lines = [l for l in text.splitlines() if l.startswith("EDGE ")]
assert len(node_lines) == len(labels)
assert len(edge_lines) == len(edges), "complete answer: edges must not be dropped"
assert "Complete answer over budget" in text
assert str(len(labels)) in text and str(len(edges)) in text
assert "raise" not in text.lower(), "must not repeat the advice that caused the overshoot"
assert "TRUNCATED" not in text and "truncated" not in text


def test_subgraph_to_text_no_overshoot_notice_when_edges_fit_too():
"""The honest-overshoot notice is additive: a complete answer that already
fits the budget must render exactly as before, with no notice at all."""
import itertools

G = nx.Graph()
labels = [f"n{i}" for i in range(3)]
for lbl in labels:
G.add_node(lbl, label=lbl, source_file="f.py", source_location="L1", community="c")
edges = list(itertools.combinations(labels, 2))
for u, v in edges:
G.add_edge(u, v, relation="calls", confidence="high")
text = _subgraph_to_text(G, set(G.nodes), edges, token_budget=2000)
assert "Complete answer over budget" not in text
assert "TRUNCATED" not in text and "truncated" not in text


def test_subgraph_to_text_order_is_deterministic():
"""Equal-degree nodes render in a stable order regardless of set iteration."""
G = nx.Graph()
Expand Down
Loading