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:
24
codex/cli.py
24
codex/cli.py
@@ -538,7 +538,12 @@ def graph_report(
|
||||
"""Show citation graph report: top hub papers, dangling citations, cluster summary."""
|
||||
from codex.config import get_settings
|
||||
from codex.db import get_conn
|
||||
from codex.graph import build_citation_graph, citation_pagerank, dangling_citations
|
||||
from codex.graph import (
|
||||
build_citation_graph,
|
||||
citation_pagerank,
|
||||
citing_coverage,
|
||||
dangling_citations,
|
||||
)
|
||||
|
||||
settings = get_settings()
|
||||
with get_conn() as conn:
|
||||
@@ -553,6 +558,8 @@ def graph_report(
|
||||
pr = citation_pagerank(graph)
|
||||
hubs = sorted(pr.items(), key=lambda x: -x[1])[:top_n]
|
||||
dangling = sorted(dangling_citations(graph, known_ids))
|
||||
n_citing, _ = citing_coverage(graph, known_ids)
|
||||
coverage = n_citing / n_papers if n_papers else 0.0
|
||||
|
||||
if output_json:
|
||||
typer.echo(
|
||||
@@ -560,6 +567,11 @@ def graph_report(
|
||||
{
|
||||
"nodes": graph.number_of_nodes(),
|
||||
"edges": graph.number_of_edges(),
|
||||
"citing_coverage": {
|
||||
"citing": n_citing,
|
||||
"papers": n_papers,
|
||||
"fraction": round(coverage, 4),
|
||||
},
|
||||
"hubs": [{"paper_id": pid, "pagerank": score} for pid, score in hubs],
|
||||
"dangling_count": len(dangling),
|
||||
"dangling": dangling,
|
||||
@@ -575,7 +587,17 @@ def graph_report(
|
||||
f"(recommended ≥ {settings.graph_min_corpus_size} for meaningful ranking).",
|
||||
err=True,
|
||||
)
|
||||
# R-D: low citing-paper coverage starves PageRank/coupling even at a healthy
|
||||
# paper count — warn on the share of papers that actually have out-edges.
|
||||
if coverage < settings.graph_min_citing_coverage:
|
||||
typer.echo(
|
||||
f"Warning: only {n_citing}/{n_papers} papers ({coverage:.0%}) have out-edges "
|
||||
f"(recommended ≥ {settings.graph_min_citing_coverage:.0%}); "
|
||||
f"low citing coverage weakens PageRank/coupling.",
|
||||
err=True,
|
||||
)
|
||||
typer.echo(f"Graph: {graph.number_of_nodes()} nodes, {graph.number_of_edges()} edges")
|
||||
typer.echo(f"Citing coverage: {n_citing}/{n_papers} papers with out-edges ({coverage:.0%})")
|
||||
typer.echo("")
|
||||
typer.echo(f"TOP-{top_n} HUB PAPERS (PageRank)")
|
||||
typer.echo("-" * 48)
|
||||
|
||||
@@ -310,6 +310,18 @@ class Settings(BaseSettings):
|
||||
),
|
||||
)
|
||||
|
||||
graph_min_citing_coverage: float = Field(
|
||||
default=0.8,
|
||||
ge=0.0,
|
||||
le=1.0,
|
||||
description=(
|
||||
"Minimum share of ingested papers that must have ≥1 out-edge (i.e. "
|
||||
"cite something) before `codex graph report` warns. Low citing "
|
||||
"coverage starves PageRank/coupling even when the paper count looks "
|
||||
"healthy — the share of *citing* papers is what matters (R-D)."
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@lru_cache(maxsize=1)
|
||||
def get_settings() -> Settings:
|
||||
|
||||
@@ -100,6 +100,20 @@ def citation_pagerank(graph: nx.DiGraph, *, damping: float = 0.85) -> dict[str,
|
||||
return cast(dict[str, float], nx.pagerank(graph, alpha=damping))
|
||||
|
||||
|
||||
def citing_coverage(graph: nx.DiGraph, known_ids: set[str]) -> tuple[int, int]:
|
||||
"""Return ``(papers_with_out_edges, total_papers)`` — the citing-paper coverage.
|
||||
|
||||
A paper "covers" the citation graph when it has ≥1 out-edge (it cites at least
|
||||
one work). Low citing coverage starves the F-15 layer (PageRank / coupling /
|
||||
co-citation) even when the total paper count looks healthy, because those
|
||||
metrics are driven by out-edges, not by node count. Surfaced by
|
||||
``codex graph report`` (R-D); the warning threshold is
|
||||
:attr:`codex.config.Settings.graph_min_citing_coverage`.
|
||||
"""
|
||||
n_citing = sum(1 for pid in known_ids if pid in graph and graph.out_degree(pid) > 0)
|
||||
return n_citing, len(known_ids)
|
||||
|
||||
|
||||
def find_related(paper_id: str, graph: nx.DiGraph, *, min_shared: int = 2) -> list[str]:
|
||||
"""Bibliographic coupling: papers sharing ≥ ``min_shared`` references with ``paper_id``.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user