feat(papers): render_paper_pdf (Markdown IMRaD → PDF) + agente paper-reviewer
Subsistema papers/: pieza de entrega + control de calidad. - render_paper_pdf_py_datascience (Python, impure, dominio datascience, grupo `papers`): convierte papers/<slug>/paper.md (frontmatter YAML + cuerpo IMRaD) en papers/<slug>/out/paper.pdf. Reutiliza el motor de paginación de flujo del paquete automatic_eda (matplotlib PdfPages, el mismo PDF móvil A5 de los informes EDA) — no reimplementa paginación ni toca matplotlib, y no añade dependencias. Cada sección IMRaD (# H1) → un Chapter en página nueva; portada desde el frontmatter (title/authors/date europea/abstract); detecta las imágenes Markdown  que el motor no entiende y las parte en bloques Image resueltos contra base_dir y base_dir/figures/. dict-no-throw estricto. 5 tests verdes (golden + edges: sin frontmatter, path inexistente, figura inexistente, ruta directa al .md). - .claude/agents/paper-reviewer: revisor académico adversarial read-only (gate anti paper-mill). Puntúa novedad/rigor/reproducibilidad/validez (0-5), intenta refutar cada claim contra la evidencia citada, detecta HARKing contra el preregistration.md, exige limitaciones declaradas y claims ≤ evidencia, y emite veredicto estructurado JSON (accept|major_revision|reject) con default conservador. Tools: Read, Grep, Glob, Bash (sin Edit/Write: solo juzga). Diseño completo: reports/0001-2026-06-30-papers-system-design.md (agente C). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,96 @@
|
||||
---
|
||||
name: render_paper_pdf
|
||||
kind: function
|
||||
lang: py
|
||||
domain: datascience
|
||||
version: "1.0.0"
|
||||
purity: impure
|
||||
signature: "def render_paper_pdf(paper_dir: str) -> dict"
|
||||
description: "Convierte un paper académico IMRaD escrito en Markdown (papers/<slug>/paper.md, con frontmatter YAML opcional title/authors/date/abstract + cuerpo) en un PDF papers/<slug>/out/paper.pdf. REUTILIZA el paginador de flujo del paquete automatic_eda (el mismo motor del PDF móvil A5 de los informes EDA): no reimplementa paginación ni toca matplotlib. Cada sección IMRaD (encabezado de nivel 1, p.ej. # Introduction, # Methods) se mapea a un Chapter que empieza en página nueva; el motor parsea por sí mismo headings, listas, tablas pipe, párrafos y **negrita** dentro del texto. Como el motor NO entiende la sintaxis de imagen Markdown , esta función detecta esas líneas y las parte en bloques Image separados, resolviendo el src relativo a base_dir y base_dir/figures/. La portada (si hay título) lista autores y fecha (DD/MM/AAAA si parseable) más el abstract. dict-no-throw: nunca lanza, devuelve {status, pdf_path, n_pages, note}."
|
||||
tags: [papers, pdf, academic, render, report, imrad, mobile, automatic-eda, markdown, no-cut, matplotlib, datascience, python]
|
||||
uses_functions: []
|
||||
uses_types: []
|
||||
returns: []
|
||||
returns_optional: false
|
||||
error_type: "error_go_core"
|
||||
imports: [os, re, datetime, yaml, "datascience.automatic_eda"]
|
||||
params:
|
||||
- name: paper_dir
|
||||
desc: "ruta al directorio del paper (papers/<slug>/, del que se lee paper.md) O directamente la ruta a un archivo paper.md (cualquier ruta terminada en .md). El directorio base para resolver figuras y escribir el PDF es el dirname del paper.md. Si el paper.md no existe (incluida una ruta totalmente inexistente) devuelve status='error' sin crash."
|
||||
output: "dict (nunca lanza): {status: 'ok'|'error', pdf_path: str|None, n_pages: int, note: str}. En éxito status='ok', pdf_path es la ruta del PDF escrito (<base_dir>/out/paper.pdf) y n_pages el total de páginas. En error status='error', pdf_path=None, n_pages=0 y note explica la causa (paper.md no encontrado, fallo del motor, o excepción inesperada)."
|
||||
tested: true
|
||||
tests: ["test_golden_genera_pdf_con_portada_y_secciones", "test_edge_sin_frontmatter_ni_figuras", "test_edge_path_inexistente_no_revienta", "test_edge_figura_inexistente_degrada", "test_acepta_ruta_directa_al_md"]
|
||||
test_file_path: "python/functions/datascience/render_paper_pdf_test.py"
|
||||
file_path: "python/functions/datascience/render_paper_pdf.py"
|
||||
---
|
||||
|
||||
## Ejemplo
|
||||
|
||||
```python
|
||||
from datascience import render_paper_pdf
|
||||
|
||||
# Estructura del paper:
|
||||
# papers/zz-demo/paper.md (frontmatter YAML + cuerpo IMRaD)
|
||||
# papers/zz-demo/figures/fig1.png (figuras referenciadas con )
|
||||
#
|
||||
# paper.md:
|
||||
# ---
|
||||
# title: A Minimal IMRaD Paper
|
||||
# authors: [Ada Lovelace, Alan Turing]
|
||||
# date: 2026-06-30
|
||||
# abstract: Demostramos que el motor pagina un paper sin cortar nada.
|
||||
# ---
|
||||
# # Introduction
|
||||
# Texto con **negrita** y una lista:
|
||||
# - Punto uno.
|
||||
# 
|
||||
# # Methods
|
||||
# | Métrica | Valor |
|
||||
# | --- | --- |
|
||||
# | Precisión | 0.91 |
|
||||
|
||||
res = render_paper_pdf("papers/zz-demo")
|
||||
print(res["status"], res["n_pages"], res["pdf_path"])
|
||||
# -> ok 3 papers/zz-demo/out/paper.pdf
|
||||
|
||||
# También acepta la ruta directa al .md:
|
||||
render_paper_pdf("papers/zz-demo/paper.md")
|
||||
```
|
||||
|
||||
## Cuando usarla
|
||||
|
||||
Cuando tengas un paper académico (o cualquier documento IMRaD) escrito en
|
||||
Markdown y quieras un **PDF móvil A5 listo para leer**, sin montar LaTeX ni
|
||||
configurar un pipeline de pandoc. Úsala después de redactar `paper.md` con su
|
||||
frontmatter (título, autores, fecha, abstract) y secciones de nivel 1; obtienes
|
||||
`out/paper.pdf` con portada, una página nueva por sección IMRaD, tablas que se
|
||||
parten repitiendo la cabecera y figuras escaladas para caber enteras —
|
||||
garantía de no-corte heredada del motor `automatic_eda`. Es la capa de
|
||||
presentación PDF del grupo `papers`.
|
||||
|
||||
## Gotchas
|
||||
|
||||
- **Impura**: escribe `out/paper.pdf` (y crea el directorio `out/`) junto al
|
||||
`paper.md`. Necesita **matplotlib** instalado en el venv (lo usa el motor
|
||||
`automatic_eda.render_pdf` con backend headless `Agg`; corre en agentes/CI sin
|
||||
display). `pyyaml` es opcional: si falta, el frontmatter se parsea con un
|
||||
parser line-based `clave: valor` degradado.
|
||||
- **Reutiliza el motor `automatic_eda.render_pdf`**: NO reimplementa paginación
|
||||
ni toca matplotlib. `render_pdf` no tiene ID propio en el registry (es parte
|
||||
del paquete de soporte `automatic_eda`), por eso `uses_functions` queda vacío;
|
||||
la dependencia real es ese motor del paquete.
|
||||
- **Nunca lanza** (dict-no-throw): `paper.md` inexistente → `{status:"error",
|
||||
pdf_path:None, note:"paper.md no encontrado: ..."}`; cualquier excepción
|
||||
inesperada → `{status:"error", note:"fallo: ..."}`. Frontmatter ausente o
|
||||
incompleto degrada limpio (sin portada, el cuerpo entero se pagina).
|
||||
- **Figuras relativas a `figures/`**: el `src` de `` se resuelve
|
||||
probando `<base_dir>/<src>` y `<base_dir>/figures/<basename>`; usa el primero
|
||||
que exista. Si ninguno existe, el motor **degrada** dibujando
|
||||
"(imagen no encontrada: ...)" — el PDF se genera igual, no crashea. Las URLs
|
||||
`http(s)` se dejan como texto Markdown, no se descargan.
|
||||
- **Solo imágenes en línea propia**: el motor `_place_markdown` NO entiende
|
||||
``; esta función solo convierte a `Image` las líneas cuyo único
|
||||
contenido es la imagen. Una imagen embebida a mitad de un párrafo se quedaría
|
||||
como texto crudo.
|
||||
- **A5 portrait mobile-first**: el formato (tamaño de página, tipografía, pie
|
||||
`Capítulo · vX.Y.Z`) lo fija el motor EDA y no es configurable desde aquí.
|
||||
Reference in New Issue
Block a user