from __future__ import annotations import datetime import json import logging import secrets from pathlib import Path from paperforge.config import paperforge_paths logger = logging.getLogger(__name__) def _logs_dir(vault: Path) -> Path: paths = paperforge_paths(vault) return paths["paperforge"] / "logs" def _ensure_logs_dir(vault: Path) -> Path: log_dir = _logs_dir(vault) log_dir.mkdir(parents=True, exist_ok=True) return log_dir # ── Reading Log ──────────────────────────────────────────────────────────── def get_reading_log_path(vault: Path) -> Path: return _logs_dir(vault) / "reading-log.jsonl" def append_reading_note( vault: Path, paper_id: str, section: str, excerpt: str, usage: str = "", context: str = "", note: str = "", project: str = "", tags: list[str] | None = None, agent: str = "", ) -> dict: if not paper_id: return {"ok": False, "error": "paper_id is required"} if not excerpt: return {"ok": False, "error": "excerpt is required"} date_str = datetime.date.today().strftime("%Y%m%d") entry_id = f"rln_{date_str}_{secrets.token_hex(4)}" now = datetime.datetime.now(datetime.timezone.utc).isoformat() entry: dict[str, object] = { "id": entry_id, "created_at": now, "paper_id": paper_id, "section": section, "excerpt": excerpt, "usage": usage, "context": context, "note": note, "project": project, "tags": tags or [], "agent": agent, "verified": False, } log_dir = _ensure_logs_dir(vault) filepath = log_dir / "reading-log.jsonl" try: with filepath.open("a", encoding="utf-8") as f: f.write(json.dumps(entry, ensure_ascii=False) + "\n") except OSError as e: return {"ok": False, "error": str(e)} return {"ok": True, "id": entry_id, "path": str(filepath)} def _read_jsonl(filepath: Path) -> list[dict]: if not filepath.exists(): return [] entries: list[dict] = [] with filepath.open("r", encoding="utf-8") as f: for line_no, line in enumerate(f, 1): stripped = line.strip() if not stripped: continue try: entries.append(json.loads(stripped)) except json.JSONDecodeError: logger.warning( "Skipping malformed JSON line %d in %s", line_no, filepath ) return entries def read_all_reading_notes(vault: Path) -> list[dict]: filepath = get_reading_log_path(vault) return _read_jsonl(filepath) def get_reading_notes_for_paper(vault: Path, paper_id: str) -> list[dict]: all_notes = read_all_reading_notes(vault) return [n for n in all_notes if n.get("paper_id") == paper_id] # ── Project Log ──────────────────────────────────────────────────────────── def get_project_log_path(vault: Path) -> Path: return _logs_dir(vault) / "project-log.jsonl" def append_project_entry(vault: Path, entry: dict) -> dict: date_str = datetime.date.today().strftime("%Y%m%d") entry_id = f"plog_{date_str}_{secrets.token_hex(4)}" now = datetime.datetime.now(datetime.timezone.utc).isoformat() record: dict[str, object] = { "id": entry_id, "created_at": now, "project": entry.get("project", ""), "date": entry.get("date", ""), "type": entry.get("type", ""), "title": entry.get("title", ""), "decisions": entry.get("decisions", []), "detours": entry.get("detours", []), "reusable": entry.get("reusable", []), "todos": entry.get("todos", []), "related_papers": entry.get("related_papers", []), "tags": entry.get("tags", []), "agent": entry.get("agent", ""), } log_dir = _ensure_logs_dir(vault) filepath = log_dir / "project-log.jsonl" try: with filepath.open("a", encoding="utf-8") as f: f.write(json.dumps(record, ensure_ascii=False) + "\n") except OSError as e: return {"ok": False, "error": str(e)} return {"ok": True, "id": entry_id, "path": str(filepath)} def read_all_project_entries(vault: Path) -> list[dict]: filepath = get_project_log_path(vault) return _read_jsonl(filepath) def get_project_entries(vault: Path, project: str) -> list[dict]: all_entries = read_all_project_entries(vault) return [e for e in all_entries if e.get("project") == project] # ── Correction Log ────────────────────────────────────────────────────────── def get_correction_log_path(vault: Path) -> Path: return _logs_dir(vault) / "correction-log.jsonl" def append_correction( vault: Path, paper_id: str, original_id: str, correction: str, reason: str = "", agent: str = "", ) -> dict: """Append a correction record to correction-log.jsonl.""" if not paper_id: return {"ok": False, "error": "paper_id is required"} if not original_id: return {"ok": False, "error": "original_id is required"} if not correction: return {"ok": False, "error": "correction is required"} date_str = datetime.date.today().strftime("%Y%m%d") entry_id = f"corr_{date_str}_{secrets.token_hex(4)}" now = datetime.datetime.now(datetime.timezone.utc).isoformat() entry: dict[str, object] = { "id": entry_id, "event_type": "correction", "created_at": now, "paper_id": paper_id, "original_id": original_id, "correction": correction, "reason": reason, "agent": agent, } log_dir = _ensure_logs_dir(vault) filepath = log_dir / "correction-log.jsonl" try: with filepath.open("a", encoding="utf-8") as f: f.write(json.dumps(entry, ensure_ascii=False) + "\n") except OSError as e: return {"ok": False, "error": str(e)} return {"ok": True, "id": entry_id, "path": str(filepath)} def read_all_corrections(vault: Path) -> list[dict]: """Read all correction entries from correction-log.jsonl.""" filepath = get_correction_log_path(vault) return _read_jsonl(filepath) def get_corrections_for_paper(vault: Path, paper_id: str) -> list[dict]: """Get all corrections for a specific paper.""" all_corrections = read_all_corrections(vault) return [c for c in all_corrections if c.get("paper_id") == paper_id]