diff --git a/paperforge/cli.py b/paperforge/cli.py index 90ce6353..44d74d31 100644 --- a/paperforge/cli.py +++ b/paperforge/cli.py @@ -285,6 +285,10 @@ def build_parser() -> argparse.ArgumentParser: p_paper_status.add_argument("query", help="Paper identifier (zotero_key, DOI, title, alias)") p_paper_status.add_argument("--json", action="store_true", help="Output as JSON") + p_pc = sub.add_parser("paper-context", help="Get full context for a paper (metadata + reading notes + corrections)") + p_pc.add_argument("key", help="Zotero key") + p_pc.add_argument("--json", action="store_true", help="Output as JSON") + p_rl = sub.add_parser("reading-log", help="Record or export reading notes") p_rl.add_argument("--write", dest="paper_id", help="Write note for this zotero_key") p_rl.add_argument("--section", help="Section (e.g. Discussion P12)") @@ -554,6 +558,11 @@ def main(argv: list[str] | None = None) -> int: return run(args) + if args.command == "paper-context": + from paperforge.commands.paper_context import run + + return run(args) + if args.command == "reading-log": from paperforge.commands.reading_log import run diff --git a/paperforge/commands/paper_context.py b/paperforge/commands/paper_context.py new file mode 100644 index 00000000..ff705666 --- /dev/null +++ b/paperforge/commands/paper_context.py @@ -0,0 +1,115 @@ +from __future__ import annotations + +import argparse +import json +import sys + +from paperforge import __version__ as PF_VERSION +from paperforge.core.errors import ErrorCode +from paperforge.core.result import PFError, PFResult +from paperforge.memory.db import get_connection, get_memory_db_path +from paperforge.memory.permanent import get_reading_notes_for_paper + + +def _build_paper_context(vault, key: str) -> dict | None: + """Build full context for a paper: metadata + reading notes + corrections.""" + + db_path = get_memory_db_path(vault) + if not db_path.exists(): + return None + + conn = get_connection(db_path, read_only=True) + try: + row = conn.execute( + """SELECT zotero_key, citation_key, title, year, doi, journal, + first_author, domain, collection_path, has_pdf, + ocr_status, analyze, deep_reading_status, lifecycle, + next_step, pdf_path, note_path, fulltext_path, paper_root + FROM papers WHERE zotero_key = ?""", + (key,), + ).fetchone() + + if not row: + return None + + paper = dict(row) + + prior_notes = get_reading_notes_for_paper(vault, key) + + corrections = [] + corr_rows = conn.execute( + """SELECT created_at, payload_json + FROM paper_events + WHERE paper_id = ? AND event_type = 'correction_note' + ORDER BY created_at DESC""", + (key,), + ).fetchall() + for cr in corr_rows: + payload = json.loads(cr["payload_json"]) + corrections.append({ + "created_at": cr["created_at"], + "previous_note_id": payload.get("ref_id", ""), + "correction": payload.get("correction", ""), + "reason": payload.get("reason", ""), + }) + + recheck_targets = [] + for n in prior_notes: + if not n.get("verified", False): + recheck_targets.append( + f"{n.get('section', 'unknown')}: {n.get('excerpt', '')[:80]}..." + ) + + return { + "warning": "Prior reading notes are not verified facts. Re-check source before reuse.", + "paper": paper, + "prior_notes": prior_notes, + "corrections": corrections, + "recheck_targets": recheck_targets, + } + finally: + conn.close() + + +def run(args: argparse.Namespace) -> int: + vault = args.vault_path + key = args.key + + context = _build_paper_context(vault, key) + + if context is None: + result = PFResult( + ok=False, + command="paper-context", + version=PF_VERSION, + error=PFError( + code=ErrorCode.PATH_NOT_FOUND, + message=f"No paper found for key: {key}", + ), + ) + else: + result = PFResult( + ok=True, + command="paper-context", + version=PF_VERSION, + data=context, + ) + + if args.json: + print(result.to_json()) + else: + if result.ok: + p = result.data["paper"] + print(f"Paper: {p.get('title', key)}") + print(f" Key: {p.get('zotero_key', '')}") + print(f" OCR: {p.get('ocr_status', 'unknown')}") + print(f" Lifecycle: {p.get('lifecycle', '')}") + notes = result.data.get("prior_notes", []) + print(f" Reading notes: {len(notes)}") + print(f" Corrections: {len(result.data.get('corrections', []))}") + if result.data.get("recheck_targets"): + print(f" Recheck targets: {len(result.data['recheck_targets'])}") + else: + print(f"Error: {result.error.message}", file=sys.stderr) + + return 0 if result.ok else 1