feat(cli): graph-report titles + JSON warning; wire recommend/cocited (U-1,U-3,U-4,C-9)

- U-1: 'graph report' shows the paper title next to each in-KB hub (dangling
  OpenAlex-id hubs are flagged), instead of bare ids.
- U-3: '--json' now includes a 'small_corpus_warning' field (null unless below
  graph_min_corpus_size) instead of silently dropping the warning.
- C-9: new 'codex discover recommend <id>' wires semanticscholar.fetch_recommendations
  (was implemented + tested but unreachable).
- U-4: new 'codex graph cocited <id>' wires graph.find_co_cited (likewise).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Tarik Moussa
2026-06-15 21:43:19 +02:00
parent c7fae5c877
commit 92928bab99
2 changed files with 62 additions and 11 deletions

View File

@@ -188,6 +188,24 @@ def discover_leads(
typer.echo(f"{item['pull']:>4}× {item['cited_id']}")
@discover_app.command("recommend")
def discover_recommend(
paper_id: str = typer.Argument(
..., help="Semantic Scholar paper ID (or arXiv:/DOI: prefixed)."
),
limit: int = typer.Option(20, "--limit", "-n", help="Number of recommendations."),
) -> None:
"""Recommended papers for PAPER_ID via Semantic Scholar."""
from codex.sources.semanticscholar import fetch_recommendations
rec = fetch_recommendations(paper_id, limit=limit)
if not rec:
typer.echo(f"No recommendations found for {paper_id}.")
return
for pid in rec:
typer.echo(pid)
@discover_app.command("citing")
def discover_citing(paper_id: str = typer.Argument(...)) -> None:
"""List papers that cite PAPER_ID."""
@@ -582,7 +600,9 @@ def graph_report(
settings = get_settings()
with get_conn() as conn:
graph = build_citation_graph(conn)
known_ids = {row["id"] for row in conn.execute("SELECT id FROM papers").fetchall()}
paper_rows = conn.execute("SELECT id, title FROM papers").fetchall()
known_ids = {row["id"] for row in paper_rows}
titles = {row["id"]: row["title"] for row in paper_rows}
if graph.number_of_nodes() == 0:
typer.echo("Citation graph is empty — ingest some papers first.")
@@ -592,6 +612,13 @@ 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))
small_corpus = n_papers < settings.graph_min_corpus_size
warning = (
f"only {n_papers} ingested papers; recommended >= {settings.graph_min_corpus_size} "
"for meaningful ranking"
if small_corpus
else None
)
if output_json:
typer.echo(
@@ -599,7 +626,11 @@ def graph_report(
{
"nodes": graph.number_of_nodes(),
"edges": graph.number_of_edges(),
"hubs": [{"paper_id": pid, "pagerank": score} for pid, score in hubs],
"small_corpus_warning": warning, # null unless below threshold (audit U-3)
"hubs": [
{"paper_id": pid, "title": titles.get(pid), "pagerank": score}
for pid, score in hubs
],
"dangling_count": len(dangling),
"dangling": dangling,
},
@@ -608,18 +639,16 @@ def graph_report(
)
return
if n_papers < settings.graph_min_corpus_size:
typer.echo(
f"Warning: only {n_papers} ingested papers "
f"(recommended ≥ {settings.graph_min_corpus_size} for meaningful ranking).",
err=True,
)
if warning:
typer.echo(f"Warning: {warning}.", err=True)
typer.echo(f"Graph: {graph.number_of_nodes()} nodes, {graph.number_of_edges()} edges")
typer.echo("")
typer.echo(f"TOP-{top_n} HUB PAPERS (PageRank)")
typer.echo("-" * 48)
for pid, score in hubs:
typer.echo(f" {score:.4f} {pid}")
# in-KB hubs show their title; dangling (OpenAlex-id) hubs are flagged (U-1)
label = titles.get(pid) or "(dangling — not ingested)"
typer.echo(f" {score:.4f} {pid} {label}")
typer.echo("")
typer.echo(f"DANGLING CITATIONS ({len(dangling)} cited but not ingested)")
typer.echo("-" * 48)
@@ -652,3 +681,23 @@ def graph_related(
)
for pid in related:
typer.echo(f" {pid}")
@graph_app.command("cocited")
def graph_cocited(
paper_id: str = typer.Argument(..., help="Paper ID to find co-cited papers for."),
) -> None:
"""List papers frequently co-cited with PAPER_ID (graph co-citation)."""
from codex.db import get_conn
from codex.graph import build_citation_graph, find_co_cited
with get_conn() as conn:
graph = build_citation_graph(conn)
results = find_co_cited(paper_id, graph)
if not results:
typer.echo(f"No papers co-cited with {paper_id}.")
return
typer.echo(f"Papers co-cited with {paper_id} (count desc):")
for pid, count in results:
typer.echo(f" {count:>3}× {pid}")