feat: add paper-context CLI command for reading-log safety loop

This commit is contained in:
Research Assistant 2026-05-14 17:44:20 +08:00
parent 6ccce87272
commit 1284dfcd2f
2 changed files with 124 additions and 0 deletions

View file

@ -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

View file

@ -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