mirror of
https://github.com/lllin000/PaperForge.git
synced 2026-07-22 06:50:53 +00:00
feat: upgrade reading-log to JSONL with context/tags/project fields and auto-render
This commit is contained in:
parent
60c524b012
commit
6ccce87272
3 changed files with 194 additions and 14 deletions
|
|
@ -291,6 +291,13 @@ def build_parser() -> argparse.ArgumentParser:
|
|||
p_rl.add_argument("--excerpt", help="Quoted excerpt")
|
||||
p_rl.add_argument("--usage", help="How this supports the current writing")
|
||||
p_rl.add_argument("--note", help="Optional cross-validation note")
|
||||
p_rl.add_argument("--context", help="Full paragraph containing excerpt")
|
||||
p_rl.add_argument("--tags", help="Comma-separated tags")
|
||||
p_rl.add_argument("--project", help="Associated project name")
|
||||
p_rl.add_argument("--render", action="store_true", help="Render reading-log.md for one or all projects")
|
||||
p_rl.add_argument("--correct", dest="correct_id", help="ID of prior reading note to correct")
|
||||
p_rl.add_argument("--correction", help="Correction text")
|
||||
p_rl.add_argument("--reason", help="Reason for correction (e.g. 'Rechecked figure legend')")
|
||||
p_rl.add_argument("--since", help="Export notes since date (YYYY-MM-DD)")
|
||||
p_rl.add_argument("--limit", type=int, default=50, help="Max notes to export")
|
||||
p_rl.add_argument("--output", help="Write markdown to file")
|
||||
|
|
@ -636,11 +643,11 @@ def _cmd_paths(vault: Path, args: argparse.Namespace) -> int:
|
|||
|
||||
if args.json:
|
||||
# Output only the keys required by D-Path Output contract
|
||||
output_keys = {"vault", "worker_script", "ld_deep_script"}
|
||||
output_keys = {"vault", "worker_script", "pf_deep_script"}
|
||||
filtered = {k: v for k, v in all_paths.items() if k in output_keys}
|
||||
filtered["vault"] = str(vault.resolve())
|
||||
filtered["worker_script"] = str(paths["worker_script"].resolve())
|
||||
filtered["ld_deep_script"] = str(paths["ld_deep_script"].resolve())
|
||||
filtered["pf_deep_script"] = str(paths["pf_deep_script"].resolve())
|
||||
print(json.dumps(filtered, ensure_ascii=False, indent=2))
|
||||
else:
|
||||
for key, path_str in sorted(all_paths.items()):
|
||||
|
|
|
|||
|
|
@ -1,16 +1,21 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import datetime
|
||||
import json
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
from paperforge import __version__ as PF_VERSION
|
||||
from paperforge.config import paperforge_paths
|
||||
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.events import export_reading_log, write_reading_note
|
||||
|
||||
from paperforge.memory.events import export_reading_log, write_correction_note, write_reading_note
|
||||
from paperforge.memory.permanent import (
|
||||
append_reading_note,
|
||||
read_all_reading_notes,
|
||||
)
|
||||
|
||||
_HEADER_RE = re.compile(r"^## ([A-Z0-9]{8}) \u2014 .+ \d{4}$")
|
||||
_TITLE_RE = re.compile(r"^\*\*Title:\*\* (.+)")
|
||||
|
|
@ -211,6 +216,72 @@ def lookup_paper_events(vault: Path, key: str) -> dict:
|
|||
conn.close()
|
||||
|
||||
|
||||
def _render_reading_log_md(vault: Path, project: str = "") -> None:
|
||||
"""Render reading-log.md from JSONL source of truth.
|
||||
|
||||
Groups notes by paper_id and writes a formatted markdown file.
|
||||
If project is specified, writes to <resources>/Projects/<project>/reading-log.md.
|
||||
Otherwise writes to <paperforge>/logs/rendered/reading-log.md.
|
||||
"""
|
||||
paths = paperforge_paths(vault)
|
||||
notes = read_all_reading_notes(vault)
|
||||
|
||||
if not notes:
|
||||
print("No reading notes to render.")
|
||||
return
|
||||
|
||||
if project:
|
||||
notes = [n for n in notes if n.get("project") == project]
|
||||
|
||||
if not notes:
|
||||
print(f"No reading notes found{' for project ' + project if project else ''}.")
|
||||
return
|
||||
|
||||
grouped: dict[str, list[dict]] = {}
|
||||
for n in notes:
|
||||
pid = n.get("paper_id", "unknown")
|
||||
grouped.setdefault(pid, []).append(n)
|
||||
|
||||
lines: list[str] = []
|
||||
heading = f"Reading Log \u2014 {project}" if project else "Reading Log \u2014 All Projects"
|
||||
lines.append(f"# {heading}\n")
|
||||
lines.append(f"*Generated: {datetime.date.today().isoformat()} | Total entries: {len(notes)}*\n")
|
||||
|
||||
for pid, entries in sorted(grouped.items()):
|
||||
lines.append(f"## {pid}\n")
|
||||
for entry in sorted(entries, key=lambda e: (e.get("section", ""), e.get("created_at", ""))):
|
||||
section = entry.get("section", "Untitled")
|
||||
lines.append(f"### {section}")
|
||||
lines.append(f"> {entry.get('excerpt', '')}")
|
||||
if entry.get("context"):
|
||||
lines.append(">")
|
||||
lines.append(f"> {entry.get('context')}")
|
||||
lines.append("")
|
||||
if entry.get("usage"):
|
||||
lines.append(f"- **Usage:** {entry.get('usage')}")
|
||||
if entry.get("note"):
|
||||
lines.append(f"- **Note:** {entry.get('note')}")
|
||||
tag_list = entry.get("tags", [])
|
||||
if tag_list:
|
||||
lines.append(f"- **Tags:** {', '.join(tag_list)}")
|
||||
verified = entry.get("verified", False)
|
||||
lines.append(f"- **Verified:** {'Yes' if verified else 'No'}")
|
||||
lines.append("")
|
||||
lines.append("---\n")
|
||||
|
||||
if project:
|
||||
output_dir = paths["resources"] / "Projects" / project
|
||||
output_dir.mkdir(parents=True, exist_ok=True)
|
||||
output_path = output_dir / "reading-log.md"
|
||||
else:
|
||||
output_dir = paths["paperforge"] / "logs" / "rendered"
|
||||
output_dir.mkdir(parents=True, exist_ok=True)
|
||||
output_path = output_dir / "reading-log.md"
|
||||
|
||||
output_path.write_text("\n".join(lines), encoding="utf-8")
|
||||
print(f"Rendered {len(notes)} entries to {output_path}")
|
||||
|
||||
|
||||
def run(args: argparse.Namespace) -> int:
|
||||
vault = args.vault_path
|
||||
|
||||
|
|
@ -265,22 +336,91 @@ def run(args: argparse.Namespace) -> int:
|
|||
print(f"No entries found for key: {args.lookup}")
|
||||
return 0
|
||||
|
||||
if args.paper_id and args.excerpt:
|
||||
ok = write_reading_note(
|
||||
vault, args.paper_id, args.section or "",
|
||||
args.excerpt, args.usage or "", args.note or "",
|
||||
if args.render:
|
||||
_render_reading_log_md(vault, args.project or "")
|
||||
return 0
|
||||
|
||||
if args.correct_id:
|
||||
if not args.correction:
|
||||
result = PFResult(
|
||||
ok=False, command="reading-log", version=PF_VERSION,
|
||||
data={},
|
||||
error=PFError(code=ErrorCode.INVALID_INPUT,
|
||||
message="--correction is required with --correct"),
|
||||
)
|
||||
if args.json:
|
||||
print(result.to_json())
|
||||
else:
|
||||
print("Error: --correction is required with --correct")
|
||||
return 1
|
||||
|
||||
all_notes = read_all_reading_notes(vault)
|
||||
original = next((n for n in all_notes if n.get("id") == args.correct_id), None)
|
||||
paper_id = original.get("paper_id", "") if original else ""
|
||||
if not paper_id:
|
||||
result = PFResult(
|
||||
ok=False, command="reading-log", version=PF_VERSION,
|
||||
data={},
|
||||
error=PFError(code=ErrorCode.NOT_FOUND,
|
||||
message=f"Original entry {args.correct_id} not found in JSONL"),
|
||||
)
|
||||
if args.json:
|
||||
print(result.to_json())
|
||||
else:
|
||||
print(f"Error: Original entry {args.correct_id} not found in reading-log.jsonl")
|
||||
return 1
|
||||
|
||||
ok = write_correction_note(
|
||||
vault, paper_id, args.correct_id,
|
||||
args.correction, args.reason or "",
|
||||
)
|
||||
result = PFResult(
|
||||
ok=ok,
|
||||
command="reading-log",
|
||||
version=PF_VERSION,
|
||||
ok=ok, command="reading-log", version=PF_VERSION,
|
||||
data={"written": ok},
|
||||
error=PFError(code=ErrorCode.INTERNAL_ERROR, message="Failed to write") if not ok else None,
|
||||
error=PFError(code=ErrorCode.INTERNAL_ERROR,
|
||||
message="Failed to write correction") if not ok else None,
|
||||
)
|
||||
if args.json:
|
||||
print(result.to_json())
|
||||
else:
|
||||
print("Written." if ok else "Failed.")
|
||||
print(f"Correction written for {args.correct_id}." if ok else "Failed.")
|
||||
return 0 if ok else 1
|
||||
|
||||
if args.paper_id and args.excerpt:
|
||||
tags_list = [t.strip() for t in args.tags.split(",") if t.strip()] if args.tags else None
|
||||
|
||||
jsonl_result = append_reading_note(
|
||||
vault, args.paper_id, args.section or "",
|
||||
args.excerpt, args.usage or "", args.context or "",
|
||||
args.note or "", args.project or "", tags_list,
|
||||
)
|
||||
|
||||
_db_ok = write_reading_note(
|
||||
vault, args.paper_id, args.section or "",
|
||||
args.excerpt, args.usage or "", args.note or "",
|
||||
args.context or "", args.project or "", tags_list,
|
||||
)
|
||||
|
||||
ok = jsonl_result.get("ok", False)
|
||||
result = PFResult(
|
||||
ok=ok,
|
||||
command="reading-log",
|
||||
version=PF_VERSION,
|
||||
data={"written": ok, "id": jsonl_result.get("id"), "path": jsonl_result.get("path")},
|
||||
error=PFError(code=ErrorCode.INTERNAL_ERROR,
|
||||
message=jsonl_result.get("error", "Failed to write")) if not ok else None,
|
||||
)
|
||||
if args.json:
|
||||
print(result.to_json())
|
||||
else:
|
||||
if ok:
|
||||
print(f"Written. ID: {jsonl_result.get('id', 'unknown')}")
|
||||
else:
|
||||
print(f"Failed: {jsonl_result.get('error', 'unknown')}")
|
||||
|
||||
if ok and args.project:
|
||||
_render_reading_log_md(vault, args.project)
|
||||
|
||||
return 0 if ok else 1
|
||||
|
||||
notes = export_reading_log(vault, since=args.since or "", limit=args.limit or 50)
|
||||
|
|
|
|||
|
|
@ -7,7 +7,9 @@ from paperforge.memory.db import get_connection, get_memory_db_path
|
|||
|
||||
|
||||
def write_reading_note(vault: Path, paper_id: str, section: str,
|
||||
excerpt: str, usage: str = "", note: str = "") -> bool:
|
||||
excerpt: str, usage: str = "", note: str = "",
|
||||
context: str = "", project: str = "",
|
||||
tags: list[str] | None = None) -> bool:
|
||||
"""Record a reading note in paper_events."""
|
||||
db_path = get_memory_db_path(vault)
|
||||
if not db_path.exists():
|
||||
|
|
@ -18,6 +20,9 @@ def write_reading_note(vault: Path, paper_id: str, section: str,
|
|||
"excerpt": excerpt,
|
||||
"usage": usage,
|
||||
"note": note,
|
||||
"context": context,
|
||||
"project": project,
|
||||
"tags": tags or [],
|
||||
}
|
||||
conn = get_connection(db_path, read_only=False)
|
||||
try:
|
||||
|
|
@ -76,3 +81,31 @@ def export_reading_log(vault: Path, since: str = "", limit: int = 50) -> list[di
|
|||
return results
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
|
||||
def write_correction_note(vault: Path, paper_id: str, original_id: str,
|
||||
correction: str, reason: str = "") -> bool:
|
||||
"""Record a correction note for a prior reading_note event."""
|
||||
db_path = get_memory_db_path(vault)
|
||||
if not db_path.exists():
|
||||
return False
|
||||
|
||||
payload = {
|
||||
"original_id": original_id,
|
||||
"correction": correction,
|
||||
"reason": reason,
|
||||
}
|
||||
conn = get_connection(db_path, read_only=False)
|
||||
try:
|
||||
conn.execute(
|
||||
"""INSERT INTO paper_events (paper_id, event_type, payload_json)
|
||||
VALUES (?, 'correction_note', ?)""",
|
||||
(paper_id, json.dumps(payload, ensure_ascii=False)),
|
||||
)
|
||||
conn.commit()
|
||||
return True
|
||||
except Exception:
|
||||
conn.rollback()
|
||||
return False
|
||||
finally:
|
||||
conn.close()
|
||||
|
|
|
|||
Loading…
Reference in a new issue