Evernote → second-brain: prompts para LLM local (Ollama)

Contexto: migrar/complementar notas e imágenes de un export de Evernote (.enex) hacia ~/nef/second-brain (nodos con frontmatter estricto + journal). El error clásico es dejar que el modelo “invente” slugs o metadatos: por eso el pipeline separa extracción (scripts), triage (JSON estricto) y redacción (borradores que un humano aprueba). El build falla con edges colgantes, así que el vocabulario de slugs siempre va inyectado en el prompt.

Pipeline mínimo (3 pasos)

  1. Extraer, no promptear. brew install evernote2md (wormski) y: evernote2md export.enex out/ -format markdown -includeoldmedia → una nota .md por nota + carpeta media/ con imágenes reales. Nunca pasar base64 del .enex al modelo.
  2. Triage con prompt A (batch de ~10 notas, salida JSON, --format json de ollama).
  3. Redacción con prompt B solo para lo aprobado. Para imágenes: prompt C con un modelo de visión local (ollama pull qwen2.5vl:7b).

Comando tipo: cat batch.md | ollama run qwen3:8b --format json < prompt-A.md > triage.json

Vocabulario inyectado (regenerar antes de cada corrida): ls content/nodes | sed 's/\.md//' | paste -sd, - → reemplazar {{SLUGS}}. content/journal/ aún no existe; la convención propuesta es content/journal/YYYY-MM-DD.md + content/journal/media/ (build lo ignora, Quartz lo sincroniza como vault).


Prompt A — triage (por lote de notas)

Eres el archivista de un second-brain local (graph of study, Markdown + frontmatter).

Te llegan notas crudas exportadas de Evernote. Tu trabajo es SOLO clasificar, no reescribir.

Slugs de nodos EXISTENTES (única fuente de verdad; no inventes ninguno):
{{SLUGS}}

Relaciones permitidas: requires | implements | generalizes | applies | cites | contradicts
Kinds permitidos: concept | paper | tool | model | algorithm | person | dataset | experiment

Para cada nota (usa su id dado), responde un JSON array con objetos:
{
  "id": "...",
  "route": "journal" | "nuevo_nodo" | "complementa_nodo" | "borrar",
  "target_slug": "solo si complementa_nodo, DEBE estar en la lista",
  "propuesto": {"slug": "...", "title": "...", "kind": "...", "level_sugerido": 0-5},
  "una_linea": "qué es y por qué importa, en una línea",
  "imagenes": ["archivos de media/ relevantes, si los hay"],
  "confianza": 0.0-1.0
}

Criterios:
- journal = texto personal/fechado (ideas del día, estados de ánimo, bitácora) → va al journal, no al grafo.
- complementa_nodo = la nota aporta cuerpo, imagen o referencia a un nodo que YA existe.
- nuevo_nodo = concepto de estudio genuino y duradero; si no estás seguro, journal.
- borrar = logística muerta (tickets, recibos, caducos).
- Nunca propongas un slug que esté en la lista de existentes.
Responde únicamente JSON válido.

Prompt B — redacción (por ítem aprobado)

Escribe un borrador Markdown para el second-brain. No agregues nada que no esté
en la fuente; conserva la voz personal de la nota original (nada de prosa corporativa).

Tipo: {{nuevo_nodo | journal | apéndice_a_nodo}}
Fuente cruda:
{{nota}}
Imágenes disponibles (usa rutas relativas ![](media/archivo.png) donde aporten,
máx 2, cada una con pie de una línea): {{imgs}}

Contrato de frontmatter (exacto, sin campos extra):
---
title: ...
kind: concept | paper | tool | model | algorithm | person | dataset | experiment
status: unseen | learning | applying | mastered
level: 0-5
hours: 0
edges: [{ to: "slug", rel: "requires|implements|generalizes|applies|cites|contradicts" }]
tags: [...]
open_questions: [...]
---
{{para journal, el frontmatter es solo: date, tags}}

Reglas:
- Cada edge "to" DEBE existir en: {{SLUGS}}. Si la dependencia natural no existe,
  ponla en open_questions como "nodo candidato: X", no como edge.
- El primer párrafo del cuerpo = una línea concreta de qué es y por qué importa.
- La duda se conserva en open_questions; nunca se borra la incertidumbre.
- Devuelve solo el archivo, listo para git.

Prompt C — caption de imágenes (visión local, qwen2.5vl:7b)

Describe esta imagen en UNA línea factual (qué muestra, no qué sugiere).
Luego, de estos slugs del grafo de estudio, devuelve a lo sumo 2 a los que
ilustra; si ninguno encaja, devuelve [].
Slugs: {{SLUGS}}
Formato: {"caption": "...", "nodos": ["..."]}

por qué así

  • El modelo solo toca lo semántico; rutas, slugs y media los deciden scripts.
  • --format json + lista de slugs hace casi imposible el fallo de integridad referencial (el que rompe npm run build).
  • Nada se escribe directo a content/: todo cae a _stubs/evernote/ y se adopta a mano, igual que paper-scan/book-scan.
  • Human-in-the-loop por lote pequeño: 20 notas revisadas valen más que 200 autoimportadas.