feat(graph): warn on low citing-paper coverage in graph report (R-D)
Add graph.citing_coverage() (papers with >=1 out-edge / total) and a graph_min_citing_coverage setting (default 0.8). codex graph report now surfaces 'Citing coverage: N/M papers with out-edges (P%)' in text and JSON, and warns when the share is below threshold -- low citing coverage starves PageRank/coupling even when the paper count looks healthy (the DQ-1 sub-item). Separate from the existing total-count graph_min_corpus_size warning. Tests: citing_coverage unit tests + CLI tests (line shown, warning fires/suppressed, JSON field). Full suite green; ruff + mypy clean. Live: graph report shows 29/29 (100%), no warning. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -117,6 +117,36 @@ class TestGraphReport:
|
||||
assert result.exit_code == 0
|
||||
assert "Warning" in result.output or "warning" in result.output
|
||||
|
||||
def test_shows_citing_coverage_line(self):
|
||||
# _sample_graph: A, B, C all cite → 3/3 = 100%, so the line shows but no warning.
|
||||
out = self._run(known_ids=("A", "B", "C")).output
|
||||
assert "Citing coverage" in out
|
||||
assert "weakens" not in out # 100% coverage → no R-D warning
|
||||
|
||||
def test_low_citing_coverage_warning(self):
|
||||
# Only A has an out-edge; B and C are ingested but cite nothing → 1/3 = 33%.
|
||||
g = nx.DiGraph()
|
||||
g.add_edge("A", "X")
|
||||
g.add_nodes_from(["B", "C"])
|
||||
result = self._run(graph=g, known_ids=("A", "B", "C"))
|
||||
assert result.exit_code == 0
|
||||
assert "weakens PageRank" in result.output # the R-D coverage warning
|
||||
|
||||
def test_json_includes_citing_coverage(self):
|
||||
import json
|
||||
|
||||
g = nx.DiGraph()
|
||||
g.add_edge("A", "X")
|
||||
g.add_nodes_from(["B", "C"])
|
||||
conn = _make_conn_with_paper_ids("A", "B", "C")
|
||||
with (
|
||||
patch("codex.db.get_conn", side_effect=_make_conn_cm(conn)),
|
||||
patch("codex.graph.build_citation_graph", return_value=g),
|
||||
):
|
||||
result = runner.invoke(app, ["graph", "report", "--json"])
|
||||
data = json.loads(result.output)
|
||||
assert data["citing_coverage"] == {"citing": 1, "papers": 3, "fraction": 0.3333}
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# codex graph related
|
||||
|
||||
@@ -10,6 +10,7 @@ import pytest
|
||||
from codex.graph import (
|
||||
build_citation_graph,
|
||||
citation_pagerank,
|
||||
citing_coverage,
|
||||
dangling_citations,
|
||||
find_co_cited,
|
||||
find_related,
|
||||
@@ -287,3 +288,24 @@ class TestDanglingCitations:
|
||||
g = build_citation_graph(_make_conn(rows))
|
||||
dangling = dangling_citations(g, set())
|
||||
assert set(dangling) == {"A", "B"}
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# citing_coverage (R-D)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestCitingCoverage:
|
||||
def test_counts_papers_with_out_edges(self):
|
||||
# _small_graph: A→.., B→.., C→D have out-edges; D, E have none.
|
||||
g = _small_graph()
|
||||
n_citing, n_papers = citing_coverage(g, {"A", "B", "C", "D", "E"})
|
||||
assert (n_citing, n_papers) == (3, 5)
|
||||
|
||||
def test_paper_absent_from_graph_counts_as_no_out_edges(self):
|
||||
# A cites; Z was ingested but has no chunks/edges, so it isn't a graph node.
|
||||
g = _small_graph()
|
||||
assert citing_coverage(g, {"A", "Z"}) == (1, 2)
|
||||
|
||||
def test_empty(self):
|
||||
assert citing_coverage(nx.DiGraph(), set()) == (0, 0)
|
||||
|
||||
Reference in New Issue
Block a user