"""Spec paper-module layout: filename mapping and generated artifacts. Defines the canonical ``papers//`` module: the mapping from the pipeline's internal content keys to the public spec filenames, plus the deterministic builders for the new spec artifacts (``paper.md``, ``metadata.json``, ``sources.json``). Pure and testable — no I/O here; the Export agent writes. """ from __future__ import annotations from typing import Any from researchlink.schemas.paper import PaperExtraction, PaperMetadata from researchlink.schemas.provenance import MetadataProvenance #: Internal pipeline content key -> on-disk spec filename. SPEC_RENAME: dict[str, str] = { "digest.md": "summary.md", "literature-review.md": "related_work.md", "implementation-link.md": "implementation.md", "reproducibility.md": "reproduction.md", "teaching-path.md": "study_notes.md", } #: The public spec module files a complete module should contain. SPEC_FILES: tuple[str, ...] = ( "paper.md", "metadata.json", "sources.json", "bibtex.bib", "summary.md", "claims.md", "related_work.md", "implementation.md", "reproduction.md", "study_notes.md", "review.md", ) def spec_filename(internal_key: str) -> str: """Map an internal content key to its on-disk name (identity if unmapped).""" return SPEC_RENAME.get(internal_key, internal_key) def paper_markdown(meta: PaperMetadata, extraction: PaperExtraction) -> str: """Deterministic structured Markdown of the paper (verbatim extraction).""" lines = [ f"# {meta.title}", "", "> Machine-extracted paper content. Verbatim from the source PDF/text; " "no interpretation added. Fields may be incomplete — verify against the original.", "", f"- **Year:** {meta.year or '[needs-verification]'}", f"- **Venue:** {meta.venue or '[needs-verification]'}", f"- **Authors:** {', '.join(meta.authors_provisional[:12]) or '[needs-verification]'}", f"- **Paper URL:** {meta.paper_url or 'N/A'}", f"- **Code URL:** {meta.code_url or 'N/A'}", f"- **Pages extracted:** {extraction.page_count or 'unknown'}", "", "## Abstract", "", extraction.abstract or "_[needs-verification] no abstract extracted_", "", "## Section Headings", "", ] if extraction.section_headings: lines += [f"- {h}" for h in extraction.section_headings] else: lines.append("_[needs-verification] no section headings extracted_") if extraction.full_text: lines += ["", "## Full Extracted Text", "", "```text", extraction.full_text.strip(), "```"] return "\n".join(lines) + "\n" SCHEMA_VERSION = "0.1" def _confidence_label(value: float) -> str: return "high" if value >= 0.6 else "medium" if value >= 0.4 else "low" def build_metadata_json( provenance: MetadataProvenance, meta: PaperMetadata, model_slug: str = "unknown-model", ) -> tuple[dict[str, Any], dict[str, Any]]: """Split provenance into (metadata.json payload, sources.json payload). Both are versioned. metadata.json carries provenance-tracked fields (each with a human-readable confidence label); sources.json wraps the per-source records as ``{type, url, accessed_at, status}``. """ data = provenance.model_dump(mode="json") raw_sources = data.pop("sources", []) # Add a readable confidence label alongside the numeric confidence. for name in provenance.FIELD_NAMES: field = data.get(name) if isinstance(field, dict): field["confidence_label"] = _confidence_label(field.get("confidence", 0.0)) payload: dict[str, Any] = { "schema_version": SCHEMA_VERSION, "slug": meta.slug, "generated_by_model": model_slug, "conflicts": provenance.conflicts(), "fields": data, } sources_payload: dict[str, Any] = { "schema_version": SCHEMA_VERSION, "sources": [ { "type": s.get("name"), "url": s.get("url"), "accessed_at": s.get("retrieved_at"), "status": "ok" if s.get("ok", True) else "error", "error": s.get("error"), "fields": list((s.get("fields") or {}).keys()), } for s in raw_sources ], } return payload, sources_payload