mirror of
https://github.com/safishamsi/graphify.git
synced 2026-10-13 23:49:14 +00:00
fix(analyze): diversify suggested questions across signal types
Round-robin the existing question categories before truncating the result. Preserve within-category order, existing small-limit priority, candidate schema, and no-signal behavior. Fixes #3849.
Co-Authored-By: OpenAI Codex <noreply@openai.com>
(cherry picked from commit 560b88d86f)
This commit is contained in:
1 parent
b8fcd29cb1
commit
bb20ef7234
3 files changed
+73
-1
No files matched your search
@@ -93,6 +93,12 @@ See `SECURITY.md` for the full threat model.
|
||||
|
||||
## Testing
|
||||
|
||||
`analyze.suggest_questions()` interleaves candidates across question types before
|
||||
applying its result limit (seven by default). This keeps ambiguous relationships
|
||||
from hiding bridge, inferred-relationship, isolation, and low-cohesion signals.
|
||||
The existing candidate order within each type is preserved; if the limit is smaller
|
||||
than the number of available types, their generation order determines priority.
|
||||
|
||||
One test file per module under `tests/`. Run with:
|
||||
|
||||
```bash
|
||||
|
||||
+13
-1
@@ -468,6 +468,9 @@ def suggest_questions(
|
||||
Generate questions the graph is uniquely positioned to answer.
|
||||
Based on: AMBIGUOUS edges, bridge nodes, underexplored god nodes, isolated nodes.
|
||||
Each question has a 'type', 'question', and 'why' field.
|
||||
Interleave question types before applying top_n so an abundant early type
|
||||
cannot crowd out later signals. Preserve the existing order within each type
|
||||
and category generation order when the limit is smaller than the type count.
|
||||
"""
|
||||
if community_labels:
|
||||
community_labels = {int(k) if isinstance(k, str) else k: v for k, v in community_labels.items()}
|
||||
@@ -583,7 +586,16 @@ def suggest_questions(
|
||||
),
|
||||
}]
|
||||
|
||||
return questions[:top_n]
|
||||
from itertools import zip_longest
|
||||
|
||||
by_type: dict[str, list[dict]] = {}
|
||||
for question in questions:
|
||||
by_type.setdefault(question["type"], []).append(question)
|
||||
diversified = [
|
||||
question for row in zip_longest(*by_type.values())
|
||||
for question in row if question is not None
|
||||
]
|
||||
return diversified[:top_n]
|
||||
|
||||
|
||||
def graph_diff(G_old: nx.Graph, G_new: nx.Graph) -> dict:
|
||||
|
||||
@@ -603,6 +603,60 @@ def test_god_nodes_filter_is_case_insensitive():
|
||||
assert variant not in labels, f"`{variant}` should be filtered as JSON-key noise"
|
||||
|
||||
|
||||
def _question_diversity_graph():
|
||||
G = nx.Graph()
|
||||
ambiguous = [f"uncertain_{i}" for i in range(10)]
|
||||
inferred = ["inferred_a", "inferred_b"]
|
||||
disconnected = [f"disconnected_{i}" for i in range(5)]
|
||||
for node in ["hub", *ambiguous, *inferred, *disconnected]:
|
||||
G.add_node(node, label=node, source_file="src/example.py", file_type="code")
|
||||
for node in ambiguous:
|
||||
G.add_edge("hub", node, confidence="AMBIGUOUS", relation="uses")
|
||||
for node in inferred:
|
||||
G.add_edge("hub", node, confidence="INFERRED", relation="uses")
|
||||
communities = {0: ["hub"], 1: ambiguous + inferred, 2: disconnected}
|
||||
return G, communities
|
||||
|
||||
|
||||
def test_suggest_questions_does_not_starve_later_categories():
|
||||
"""#3849: ten ambiguous edges must not hide all other question types."""
|
||||
G, communities = _question_diversity_graph()
|
||||
questions = suggest_questions(G, communities, {})
|
||||
assert len(questions) == 7
|
||||
assert [q["type"] for q in questions[:5]] == [
|
||||
"ambiguous_edge", "bridge_node", "verify_inferred", "isolated_nodes", "low_cohesion",
|
||||
]
|
||||
assert questions == suggest_questions(G, communities, {})
|
||||
|
||||
|
||||
@pytest.mark.parametrize("top_n", [1, 3, 7, 100])
|
||||
def test_suggest_questions_diversity_preserves_limit_and_candidates(top_n):
|
||||
G, communities = _question_diversity_graph()
|
||||
all_questions = suggest_questions(G, communities, {}, top_n=100)
|
||||
limited = suggest_questions(G, communities, {}, top_n=top_n)
|
||||
assert limited == all_questions[:top_n]
|
||||
assert sum(q["type"] == "ambiguous_edge" for q in all_questions) == 10
|
||||
assert all(set(q) == {"type", "question", "why"} for q in all_questions)
|
||||
|
||||
|
||||
def test_suggest_questions_single_category_uses_available_slots():
|
||||
G = nx.complete_graph(5)
|
||||
for node in G:
|
||||
G.nodes[node].update(label=str(node), source_file="example.py", file_type="code")
|
||||
for u, v in G.edges:
|
||||
G.edges[u, v].update(confidence="AMBIGUOUS", relation="uses")
|
||||
questions = suggest_questions(G, {0: list(G)}, {})
|
||||
assert len(questions) == 7
|
||||
assert all(q["type"] == "ambiguous_edge" for q in questions)
|
||||
|
||||
|
||||
def test_suggest_questions_empty_graph_keeps_no_signal():
|
||||
questions = suggest_questions(nx.Graph(), {}, {})
|
||||
assert len(questions) == 1
|
||||
assert questions[0]["type"] == "no_signal"
|
||||
assert questions[0]["question"] is None
|
||||
|
||||
|
||||
def test_suggest_questions_excludes_rationale_nodes_from_isolated_count():
|
||||
G = nx.Graph()
|
||||
G.add_node("service", label="Service", file_type="code", source_file="service.py")
|
||||
|
||||
Reference in new issue
Block a user