biosemiotics

CLAUDE.md — Manual de operación del atlas biosemiotics

Este archivo le enseña a cualquier sesión de Claude Code cómo trabajar en este repositorio. Léelo completo al arrancar. No improvises el flujo: está escrito aquí por una razón.


Qué es este proyecto

Un atlas educativo de POCUS (ecografía en el punto de atención) para el médico de primer contacto, en español. Su tesis es semiótica: cada hallazgo ecográfico es un signo que une un significante (lo que se ve), un significado (la realidad clínica) y una decisión (qué cambia en el manejo).

Autor y responsable clínico: Dr. Alcy Torres. Toda decisión clínica final es suya.

Regla de oro del sistema

Una fuente, muchas salidas. El banco de archivos .qmd es la ÚNICA fuente de verdad. Todo lo demás (build/, el índice, el HTML, el XML experimental de build/jats/, el proyecto Quarto de build/quarto/) es derivado y se regenera. NUNCA edites archivos en build/ a mano: el siguiente indice.py o qmd.py los sobrescribe. Si algo está mal en una salida, se arregla en el .qmd de origen y se recompila.

Mapa del repositorio

proyecto-biosemiotics/
├── CLAUDE.md                    ← este archivo
├── mapa-maestro-biosemiotics.md ← QUÉ escribir y en qué orden (léelo siempre)
├── conceptos/*.qmd              ← el "por qué" (física, artefactos, técnica)
├── signos/*.qmd                 ← el "qué hago" (significante→significado→decisión)
├── casos/*.qmd                  ← el paciente real
├── scripts/                     ← build.py, qmd.py, epub.py, libro.py, indice.py, refs.py
├── refs.bib                     ← bibliografía (SOLO desde PubMed vía refs.py)
├── assets/                      ← plantillas para nuevo.py
└── build/                       ← GENERADO; solo se versionan index.json
                                    y atlas-inject.html

La documentación de referencia está incorporada en docs/atlas/README.md: esquema de entidades, metadatos e instructivo del artículo. Consúltala para el detalle de campos y secciones. No requiere instalar la antigua skill biosemiotics-atlas ni conservar su archivo externo.

Compilar el libro en PDF (build/libro.pdf)

python scripts/libro.py --salida build/libro.pdf --solo-publicados

El LaTeX ya no se escribe a mano. build_latex() y sus auxiliares —markdown_a_latex(), escape_latex(), figura_latex()— se jubilaron: eran ~350 líneas que reimplementaban la conversión y el escape en paralelo al ensamblado del EPUB, con el riesgo permanente de que las dos ediciones divergieran. Ahora el PDF sale del mismo proyecto Quarto que el EPUB, y libro.py solo renderiza y valida.

El compilador sigue siendo LuaLaTeX, no pdflatex. El banco escribe umbrales y decisiones con símbolos Unicode estructurales (≥, →, ±) porque así se leen en la clínica. _quarto.yml fija pdf-engine: lualatex y mainfont: FreeSerif, la única fuente disponible que cubre esos glifos sin fallback silencioso. Con pdflatex verías Missing character en cuanto el texto traiga ≥/→/±: no es un error del banco, es el compilador equivocado.

La fuente LaTeX es un entregable, no un intermedio. keep-tex la deja en build/quarto/libro.tex —no en build/libro.tex, que ya no existe— junto a las imágenes que qmd.py copió al proyecto. Ese directorio compila tal cual, sin depender del checkout:

cd build/quarto
lualatex -halt-on-error -interaction=nonstopmode libro.tex

Eso es lo que empaqueta paquete_latex.py: el proyecto entero menos _salida/. El ZIP anterior mezclaba build/libro.tex con el árbol assets/ del repositorio y traía rutas ../assets/... que solo resolvían desde build/.

Dependencias de sistema: texlive-latex-recommended, texlive-latex-extra, texlive-lang-spanish, texlive-luatex y fonts-freefont-ttf. biber ya no hace falta: el .tex de Quarto no usa biblatex porque la bibliografía se emite ya resuelta por bibliografia.referencia_ghost().

Compilar el EPUB (build/atlas.epub)

La primera edición citable incluye solo fichas que ya pasaron por la revisión editorial en Ghost:

python scripts/build.py
python scripts/epub.py --salida build/atlas.epub --solo-publicados

El manuscrito ya no se ensambla a mano. scripts/qmd.py proyecta el banco a un proyecto Quarto book en build/quarto/ (_quarto.yml + un capítulo por capítulo temático u órgano) y epub.py solo elige motor, renderiza y valida. De ese mismo árbol salen EPUB, PDF y HTML con `quarto render build/quarto –to

`; ya no hay tres renderizadores del mismo contenido. `epub.py` prefiere `quarto` y cae a `pandoc` sobre `build/quarto/libro-plano.md` —el mismo libro aplanado, derivado de las mismas funciones— cuando Quarto no está instalado. Fuerza uno u otro con `--motor quarto|pandoc`. Si las dos salidas difieren en contenido, es un fallo de `qmd.py`, no una variante editorial aceptable. **Destinos de generación:** `qmd.py --destino` y las opciones `--proyecto` de EPUB/PDF solo aceptan subdirectorios del `build/` real del repositorio; no aceptan `build/` entero, enlaces ni junctions. Un directorio no vacío debe ser un proyecto reconocido por `.biosemiotics-quarto`. Los proyectos anteriores en `build/quarto/` se reconocen por sus archivos generados característicos. El ensamblado se prepara en un temporal antes de reemplazar el proyecto previo. Si falla también la restauración, el error indica la copia conservada para recuperarla; no la borres. Esto no protege de ejecuciones concurrentes ni convierte el render posterior de Quarto en una transacción. La jerarquía es **parte (`#`) → capítulo temático u órgano (`##`) → ficha (`###`) → cuerpo (`####`)**. El ensamblado anterior ponía la ficha en `###` dejando el cuerpo en `##`, así que "La pregunta clínica" quedaba por encima del título de su propia ficha y `--split-level=2` partía el EPUB dentro de cada signo. No lo reintroduzcas. **No uses la clave `part:` de Quarto.** Su escritor de EPUB no emite páginas divisorias de parte: "Fundamentos" y las ocho partes de sistema desaparecían del contenedor y del índice, y solo sobrevivían en la barra lateral del HTML. Por eso `_quarto.yml` lleva una lista plana de capítulos y la parte se convierte en el capítulo del libro. Tampoco declares `identifier` ni `rights` bajo `book:`: no son propiedades válidas de ese esquema, y al nivel superior Quarto las pasa a pandoc *además* del `epub-metadata.xml`, dejando dos `dc:identifier` en el OPF. Las citas **no** pasan a citeproc: el banco cita poco en línea y su evidencia vive en `refs`, así que se sigue usando `bibliografia.resolver_citas()` + `bibliografia.referencia_ghost()`, que numeran por ficha y emiten el estilo de la casa con DOI y PMID. Cada ficha conserva su sección «Evidencia» y el libro cierra con la bibliografía en orden de aparición. El piso soportado del repositorio es Python 3.9. El generador requiere Python 3.9 o posterior, PyYAML y Quarto o Pandoc. El job de integridad debe probar tanto 3.9 como la versión moderna fijada en CI; el job de citas no se duplica para evitar repetir llamadas a PubMed. El generador lee el banco de forma dinámica y hereda de `configuracion.py` el orden de capítulos y sistemas; no usa listas manuales. El archivo resultante vive en `build/` y no se versiona. El workflow `.github/workflows/epub.yml` se ejecuta automáticamente en cada cambio relevante fusionado a `main`: instala Quarto, valida con EPUBCheck, compila el PDF con LuaLaTeX desde el mismo proyecto y publica EPUB, PDF, TEX y ZIP LaTeX como artifacts durante 90 días. En un release, además los adjunta al release. `workflow_dispatch` queda solo como recuperación o para generar una edición de prueba. **El EPUB es el puente con Ghost, no una copia muerta.** Cada ficha publicada abre con `*Edición en línea:* `, de modo que el lector salta del libro al artículo vivo —donde están los loops y las correcciones posteriores—. Las figuras salen como `
` con `
`, y el pie conserva fuente y licencia **como enlaces**. Esto obliga a usar el lector `markdown` de pandoc: `gfm` acepta `implicit_figures` pero la ignora, y aplana el pie a un `alt=` de texto plano, con lo que los enlaces de crédito y licencia desaparecen sin que falle nada. El CSS del EPUB **no usa unidades `vh`**: los lectores basados en Adobe Digital Editions las resuelven como 0 y la imagen queda embebida pero invisible. El OPF declara el DOI dereferenciable (`https://doi.org/…`, con `identifier-type` ONIX 06), la licencia con su URL en `dc:rights`, descripción, fuente y los términos MeSH del banco como `dc:subject`. Título, autor, fecha, idioma y editorial los aporta pandoc por `--metadata`; **no los dupliques** en `epub-metadata.xml` o el OPF sale con dos `dc:title` y dos `dc:identifier`. `validar_epub()` verifica todo esto —incluidos el recuento de `figcaption`, los enlaces a Ghost y la portada declarada— y aborta si algo se perdió. Cada figura debe declarar en `medios`: descripción, crédito, fuente y URL, licencia y URL, y `archivo_local`. La ausencia de cualquiera de esos datos o del archivo local aborta la compilación: no se omiten imágenes ni se infiere su atribución. Para una edición futura con todo el banco se omite `--solo-publicados`, únicamente después de la revisión editorial pendiente. Metadatos actuales: DOI `10.5281/zenodo.21435362`; ISBN EPUB pendiente. La portada tipográfica es original, CC BY 4.0, sin logos ni identidad visual del HECAM/IESS. El EPUB incluye el aviso de uso exclusivamente educativo y una página final de créditos de imágenes. Zenodo archiva el snapshot del repositorio, no el asset del release; incorporar el binario al registro DOI requiere una carga separada. **Cicatriz de licencia (23/08/2026).** El atlas nació CC BY-NC 4.0 y pasó a **CC BY 4.0** el 15/08/2026 por decisión explícita de Alcy (#41: "El atlas pasa a CC BY 4.0"), que tocó los seis lugares donde vivía la licencia —fichas, `indice.py`, `.zenodo.json`, README y el texto legal de `LICENSE`—. El depósito de Zenodo de v0.1.0 (19/07/2026) es **anterior** a esa decisión y quedó correctamente archivado con la licencia de su momento, CC BY-NC 4.0; lo que pasó es que ningún release posterior volvió a depositar hasta ahora, así que el DOI de concepto siguió resolviendo a esa versión desactualizada durante más de una semana. v0.2.0 (23/08/2026) cierra esa brecha: hereda `.zenodo.json` con CC BY 4.0 y es lo que resuelve hoy el DOI de concepto. Zenodo no permite editar los metadatos de una versión ya publicada, así que v0.1.0 conserva la licencia de su momento para siempre —verificable en `https://api.datacite.org/dois/10.5281/zenodo.21435363`—, y eso es correcto, no un error a corregir. **Lección:** un cambio de licencia en el repositorio no se propaga solo al DOI ya acuñado; exige un release nuevo el mismo día, o el registro citable queda diciendo algo que el repositorio ya no dice. ## Lo primero al arrancar una sesión ### Raíz y rutas de los comandos Los scripts usan por defecto la raíz real del repositorio, obtenida desde su propio archivo; el directorio actual no cambia el banco ni la carpeta `build/`. Para otro banco usa `--raiz `. Una raíz explícita relativa se resuelve desde el directorio actual; `--destino`, `--proyecto`, `--salida` y `--db` relativos se resuelven desde esa raíz. Las rutas absolutas se conservan. `indice.py ` y `atlas.py ` siguen admitiendo la forma posicional antigua, pero no se puede combinar con `--raiz`. La forma canónica es la bandera. `consultas.py` y `senuelo.py` leen una única base, `build/atlas.db` bajo la raíz, y aceptan `--db` para una ruta distinta. Si falta, fallan con la ruta exacta y el comando de compilación. `nuevo.py` escribe siempre en la raíz seleccionada. Este contrato no cambia la protección de `qmd.py`: el proyecto Quarto todavía debe estar bajo el `build/` real del banco y no se amplía ningún permiso de borrado. Instala la dependencia Python exacta y ejecuta el preflight de solo lectura: ```bash python -m pip install -r requirements.txt python scripts/preflight.py ``` Ese comando comprueba Python 3.9+, PyYAML, la estructura, el contrato editorial y la coherencia de publicación e índice. Antes de generar EPUB/PDF/LaTeX usa `python scripts/preflight.py --publicacion`; exige además Quarto, LuaLaTeX, Java, `rsvg-convert` y EPUBCheck. Las versiones de referencia viven en CI. 1. Corre `git status` y reporta el estado. Si hay cambios sin commitear, avísalo antes de empezar. 2. Lee `mapa-maestro-biosemiotics.md` y di **qué signo toca según la oleada** (no saltes de oleada sin que Alcy lo pida). 3. Corre `python scripts/build.py` y reporta las alertas actuales (qué falta: abstracts, refs, urls). ## Flujo para agregar un signo 1. **Ubícalo en el mapa maestro.** Copia su fila: `sistema`, `organo`, `nivel`, oleada. No inventes estos valores — están definidos en la taxonomía del mapa. 2. **Crea el archivo** con `python scripts/nuevo.py signo ""` o partiendo de la plantilla. 3. **Contenido:** sigue la estructura estándar del instructivo (encabezados `##` LITERALES, que el XML de intercambio mapea automáticamente). Registro: permiso para el principiante, frases cortas, español claro. 4. **Abstract obligatorio:** 40-80 palabras, patrón qué se ve → qué significa → qué decide → dónde falla. 5. **`falsos_positivos` obligatorio:** un signo sin límites enseña a reconocer sin enseñar a dudar. Distingue *falso positivo* (algo que imita el signo sin serlo) de *variante* (el signo real con otra textura) — van en campos distintos. 6. **Referencias:** ver la regla dura abajo. 7. **`url` vacía por ahora.** La plantilla ya trae el campo `url: ""`. Déjalo vacío hasta que el artículo exista en Ghost — el atlas lo mostrará como "(sin publicar)", que es la verdad. **No inventes ni adivines el slug:** el de líneas B resultó ser `lineas-b-ultrasonido-pulmonar`, no `lineas-b`. La URL la da Alcy después de publicar. 8. **Valida:** `python scripts/build.py`. No continúes con errores. ## Reglas duras (no se rompen nunca) ### Contrato editorial antes de generar `build.py`, `indice.py`, `atlas.py` y `qmd.generar()` bloquean la generación antes de escribir si alguna ficha seleccionada incumple el contrato. Los errores identifican archivo, entidad y campo o encabezado. EPUB/PDF heredan este control mediante `qmd.generar()`; `--solo-publicados` valida las fichas seleccionadas. Todos los tipos requieren título, cuerpo, nivel válido, abstract de 40–80 palabras y una lista no vacía de claves existentes en `refs.bib`. Los conceptos requieren dominio; conservan su estructura libre. Los signos requieren sistema válido, órgano, ventana, sondas, significante, significado, decisión y falsos positivos, además de las ocho secciones literales del instructivo. Los casos requieren órgano, decisión semiótica, signos y sus siete secciones. Las secciones obligatorias deben aparecer una sola vez y contener texto; los campos obligatorios no admiten `TODO`. Las listas deben contener textos no vacíos. Las fichas con `estado: borrador` pueden estar incompletas y quedan fuera de Ghost, índice, atlas HTML y Quarto. `build.py` las conserva en SQLite y el grafo de trabajo con alertas, también con `--solo db|grafo`. No distribuyas la base o el grafo de trabajo como una edición revisada. Toda ficha seleccionada para un derivado público debe cumplir el contrato editorial. Este control comprueba estructura, no calidad clínica, veracidad de citas ni autorización para publicar; una ficha estructuralmente válida aún necesita revisión clínica y editorial. ### Estado de publicación y trazabilidad `estado` es la autoridad común; la URL debe concordar con él: | Estado | URL | Salidas públicas | |---|---|---| | `borrador` | Vacía | Excluido; solo trabajo local | | `revisado` | Vacía | Ghost, índice y edición completa | | `publicado` | URL pública real | Lo anterior y edición `--solo-publicados` | Solo se pasa a `revisado` después de la aprobación editorial. Para un caso, `consentimiento: obtenido` es obligatorio tanto en `revisado` como en `publicado`; un estado público sin consentimiento hace fallar el comando antes de escribir. Tener consentimiento no convierte por sí solo un borrador en revisado. Esto no sustituye comprobar de-identificación y autorización real para publicar. Registra `fecha_revision` (YYYY-MM-DD) cuando se aprueba la ficha y `ghost_id` (ID real del post, 24 caracteres hexadecimales) cuando se crea en Ghost. La migración histórica deja ambos en `null` cuando no constan en la fuente; no usa la fecha del commit ni deduce un ID del slug. Estos campos se conservan en SQLite e índice para completarlos con evidencia durante el flujo editorial. Una fecha o ID sintácticamente válido no demuestra por sí solo una revisión. El índice añade `ghost_sha256`, calculado sobre el mismo cuerpo canónico que `build.py` deja en `build/ghost/`. `verificar_publicacion.py` vuelve a calcularlo y bloquea una deriva entre fuente, bibliografía y registro. Para comparar ese cuerpo con el editor de Ghost usa `auditar_pegado_ghost.py`. Compatibilidad: si falta `estado`, una URL implica `publicado`; sin URL se interpreta `borrador`. El booleano antiguo `publicado` solo se acepta si coincide con estado y URL, y debe retirarse al migrar. SQLite conserva esa columna como valor derivado para consultas existentes. Nunca hay dos autoridades de estado. Para retirar una ficha, cambia a `borrador`, vacía la URL y regenera: Ghost-ready, índice, atlas y libro la excluyen; el índice retira sus JSON-LD/XML anteriores. Esto no retira automáticamente un artículo ya publicado en Ghost ni una edición archivada. La operación de Ghost requiere su propio flujo editorial. ### Alcance del XML de `build/jats/` Es un intercambio experimental que conserva metadatos, secciones clínicas, medios y claves bibliográficas con vocabulario JATS. Las fichas se marcan como material educativo, no como artículos de investigación. El XML no declara una DTD, no expande las referencias y no se valida contra las reglas de PMC, Crossref ni otro repositorio. Nunca lo llames «listo para depósito»: adapta y valida cada archivo contra el perfil concreto del destino antes de enviarlo. ### Reglas clínicas y de publicación - **CITAS: solo desde PubMed, verificadas.** Usa `scripts/refs.py`. NUNCA escribas una referencia de memoria ni aceptes una que produjo un LLM sin verificar el PMID. Cualquier cifra clínica (umbral, tasa, fórmula) debe tener una fuente que la diga *exactamente*. Si un LLM "recuerda" una cita, trátala como falsa hasta probar lo contrario en PubMed. Este proyecto ya fue salvado de tres referencias inventadas — no repitas el episodio. - **Verifica que la fuente diga la cifra.** No basta con que el paper trate el tema. Abre el abstract; si dice 1.2%, tu texto dice 1.2%, no "1-4%". Ajusta el texto a la fuente, nunca al revés. - **Un DOI que Crossref no resuelve no se publica.** `verificar_citas.py` distingue un 404 (Crossref no conoce ese DOI: verificación fallida, sale con código 1) de un error de red transitorio (timeout, 429, 5xx: reintenta). Si la revista es real y simplemente no deposita en Crossref, decláralo en `refs-sin-crossref.txt` con su razón por escrito; esa exención renuncia a la segunda autoridad, así que confirma el PMID a mano antes de usarla. Lo que no se hace es dejar pasar un 404 en silencio. - **Verificación incompleta bloquea la integración.** `verificar_citas.py` es siempre estricto; `--estricto` se conserva como alias compatible. Código 0 significa verificación completa o exención declarada de Crossref con PubMed correcto; 1 indica entrada inválida o discrepancia; 2, servicio no disponible o respuesta inválida. Nunca se acepta 2 como éxito ni se añade una exención para resolver un timeout. Hay hasta tres intentos por consulta, con esperas de 1 y 2 segundos. Agotados los intentos de Crossref se detiene el banco y se informa qué quedó sin consultar; volver a ejecutar cuando se recupere el servicio. El job obligatorio de integridad exige también citas correctas en PR, ejecución manual y semanal. En push a `main` no repite la red; exige la integridad local. El job de citas tiene un límite total de diez minutos. - **Exenciones acotadas y revisadas.** Requieren una razón no vacía y una clave existente o prefijo de registrador DOI (`10.NNNN/`). Solo pueden justificar un 404 de Crossref, nunca un 400/422, un fallo de red ni un PMID/DOI inconsistente. La identidad DOI se compara conservando puntuación; las variantes de título son informativas únicamente cuando el DOI coincide en ambas autoridades. - **Sección de límites obligatoria** ("Dónde NO confiar"). Sin ella, el signo no se publica. Es el firewall clínico. - **Consentimiento antes de publicar un caso.** El consentimiento clínico para escanear NO es consentimiento para publicar: son dos "sí" distintos. Sin `consentimiento: obtenido`, el caso no se publica. Verifica de-identificación: sin DICOM metadata, sin rostro, sin identificadores, sin señalética institucional. - **Nada que implique aval del HECAM/IESS.** La plataforma es independiente. - **Imágenes: SOLO Wikimedia Commons, NUNCA generadas con IA.** La `fuente_url` de toda imagen es una página `https://commons.wikimedia.org/wiki/File:…`. No se generan, dibujan ni componen imágenes con IA, ni se toman de otro sitio aunque parezcan libres. Si Commons no tiene una adecuada, se detiene y se pregunta a Alcy. `build.py` bloquea cualquier otro origen; la única excepción es histórica (VTI, figura CC BY 4.0 de acceso abierto publicada el 2026-09-09) y vive en `validacion.IMAGENES_EXENTAS`. Una excepción nueva requiere decisión explícita de Alcy. - **Toda publicación nueva sale en web y por email a los suscriptores.** «Publish only» no es la opción del atlas: así salieron todas las publicaciones registradas desde #75 hasta #99 (11 suscriptores). La evidencia es `Published and sent`. Si Ghost no ofrece el envío, se detiene y se reporta; no se publica solo en web en silencio. Actualizar un post ya publicado no reenvía el email. - **No edites `build/` a mano.** Regenéralo. - **Casos raros → composite.** Un diagnóstico infrecuente en comunidad pequeña re-identifica. Usa caso representativo y decláralo. ## Ciclo de publicación en Ghost La skill `ghost` es la lista de ejecución de este ciclo para el rol publicador: vive en `.claude/skills/ghost/` (Claude Code) y en `.agents/skills/ghost/` (Codex), con el mismo contenido, que un test obliga a mantener idéntico. Si la skill y este manual difieren, gana este manual. Una misma sesión conserva los dos roles del ciclo, pero nunca los mezcla en un mismo paso ni en un mismo commit: - **Publicador:** opera Ghost y registra imagen, licencia, URL definitiva y salidas editoriales. - **Proveedor:** valida la fuente, regenera índice, mapa y metadatos derivados. La separación es operativa, no personal: cada rol usa su propia rama y su PR apilado. El cambio de rol ocurre solo después de crear y congelar el PR padre del publicador. Se trabaja de forma secuencial en el único worktree principal; no se crea un segundo worktree para simular otra sesión. ### Límite de responsabilidad — obligatorio Esta sección prevalece sobre las instrucciones generales de creación, compilación y Git cuando la tarea solicitada sea publicar un artículo. El publicador puede usar la sesión autorizada de Ghost y, sobre una ficha `.qmd` **ya creada, revisada y validada durante la fase proveedora**, modificar `url`, `estado` y `ghost_id` para registrar la publicación real, además de `medios`; puede añadir el archivo licenciado a `assets/img/`, ejecutar `build.py`, verificar el libro con `libro.py` y entregar esos cambios en una rama/PR de publicación. No puede crear fichas, modificar el cuerpo editorial, `refs`, PMID, DOI o Crossref, ni ejecutar `indice.py` o modificar `build/index.json`. Tampoco actualiza el mapa maestro ni purga la caché: esas acciones pertenecen al flujo separado del **proveedor del índice**. Si falta la ficha o el cuerpo canónico, o si fallan sus referencias, la fase publicadora se detiene. La misma sesión vuelve explícitamente a una rama de proveedor para crear o reparar ese contenido, lo integra y solo después inicia de nuevo la publicación. `build.py` se permite en la fase publicadora únicamente después de añadir `medios` o la URL para validar la imagen y su salida LuaLaTeX; `indice.py` sigue prohibido hasta el cambio de rol. ### 0. Preflight — evitar colisiones Antes de abrir Ghost: ```bash python scripts/auditar_pegado_ghost.py \ --canon build/ghost//.qmd ``` - Busca el título en Ghost entre borradores y publicados. Si ya existe, **detente** y abre el artículo existente; nunca crees un segundo post. - Confirma que el artefacto canónico existe y que su huella es válida. Esta auditoría es de lectura; no genera ni reescribe archivos. - Confirma que la ficha fuente ya existe y no tiene cambios editoriales pendientes. Crea una rama de publicación; en ella solo podrán cambiar `url`, `estado`, `ghost_id`, `medios`, `assets/img/` y las salidas LuaLaTeX correspondientes. ### 1. Preparar y revisar Ghost 1. Usa exclusivamente `build/ghost//.qmd` como cuerpo. No lo regeneres. Antes de tocar Ghost, guarda su huella esperada: ```bash python scripts/auditar_pegado_ghost.py --canon build/ghost//.qmd ``` **Pegado idempotente (obligatorio).** El cuerpo de Ghost usa Lexical y `fill()` sobre un editor no vacío puede **anexar** en lugar de reemplazar. Nunca repitas `fill`, `type` o pegar sobre un cuerpo que ya contiene texto. Si hay que restaurarlo: enfoca el cuerpo, `Ctrl/Cmd+A`, `Backspace`, confirma longitud cero, pega una sola vez y vuelve a leer el texto visible. Si el cuerpo ya coincide con el canónico, no lo toques. 2. Selecciona una imagen de **Wikimedia Commons** (nunca generada con IA), verifícala con `auditar_medios.py --verificar --id `, guarda una copia auditable en `assets/img/` y declárala en `medios` con `destacada: true`, descripción, crédito, fuente y URL, licencia y URL de licencia, y `archivo_local`. Esta es responsabilidad exclusiva del publicador porque debe ser exactamente la misma imagen subida a Ghost. Ejecuta `build.py` y confirma que aparece en `build/quarto/libro.tex`; compílalo con `libro.py` cuando el entorno lo permita. 3. En Ghost configura: título, cuerpo, imagen, pie y texto alternativo, tags, excerpt, autor y acceso. Meta title/description y tarjetas sociales pueden quedar vacíos solo cuando se quiere heredar título, excerpt e imagen, como en los artículos anteriores. Tags, excerpt, alt y pie **no se componen a mano**: se copian de los campos `*_esperado` que imprime `auditar_pegado_ghost.py --canon …`, calculados por `build.py` desde la ficha. El pie canónico es «descripción. crédito, vía fuente. licencia.», el formato publicado en VExUS. El pie también se reemplaza de forma idempotente: selecciona todo su valor, bórralo, confirma que quedó vacío e insértalo **una vez**. No encadenes `fill()` y `type()`. Si el control colapsado de Ghost mide 0 px, ábrelo desde la interfaz antes de escribir; nunca hagas clic por coordenadas porque puede insertar el crédito dentro del primer encabezado del cuerpo. 4. Revisa las vistas previas web y email: título, excerpt, imagen, atribución, evidencia y enlace al Reto. La revisión debe confirmar además que el contador de palabras no aumentó aproximadamente al doble y que cada encabezado canónico aparece una vez. Ante cualquier edición posterior, repite esta comprobación antes de abrir el diálogo de publicación. Para auditar una captura textual del editor: ```bash python scripts/auditar_pegado_ghost.py \ --canon build/ghost//.qmd \ --captura \ --pie "" ``` Sin `--pie-esperado`, el auditor compara con el pie canónico de la cabecera. Debe salir con código 0 antes de publicar: detecta el pie duplicado. 5. Una publicación nueva sale como **«Publish and email»**. Justo antes del último botón, confirma explícitamente con el usuario que se publicará en web **y** se enviará por email, con el número exacto de suscriptores que muestra Ghost. 6. Después de publicar, exige evidencia de Ghost (`Published and sent` en una publicación nueva; `Published` solo al actualizar un post existente) y copia la URL pública definitiva. Nunca uses la URL del editor (`/ghost/#/...`) ni una vista previa (`/p/...`). ### 2. Registrar los artefactos y cerrar la fase publicadora 1. Registra `estado: publicado`, el `ghost_id` real y la URL pública definitiva en `url` de la ficha existente. No cambies ningún otro campo salvo `medios`. 2. Ejecuta `build.py` y `libro.py`, y valida que `build/quarto/libro.tex` use exactamente el `archivo_local` subido a Ghost y compila LuaLaTeX si está disponible. La misma imagen debe poder entrar en el EPUB. **No ejecutes `indice.py` ni agregues `build/index.json`: esa regeneración sigue en el PR hijo.** 3. Abre un **PR borrador de publicación** limitado a la ficha existente, `assets/img/` y las salidas LuaLaTeX que correspondan. Es normal que el check de deriva del índice señale que aún falta la regeneración; no la resuelvas desde el rol publicador. 4. Registra en el PR el `id`, la URL definitiva y el nombre de la rama. Cambia explícitamente al rol proveedor y crea desde esa rama el PR hijo de regeneración. El traspaso no termina con una nota: debe existir ese PR hijo. 5. Devuelve un informe con: `id`, título, URL pública, id de Ghost, estado, fecha/hora, audiencia y destinatarios, tags, excerpt, autor, acceso, imagen, alt, crédito, fuente, licencia, archivos cambiados y PR. Esta división evita mezclar autoridades: la fase proveedora crea contenido y verifica PMID/Crossref; la fase publicadora nunca toca esos campos. El rol publicador aporta lo que no existe antes de Ghost —URL e imagen finales— y el rol proveedor lo propaga después a los derivados. ### 3. Traspaso atómico del índice — obligatorio Para que el buscador nunca quede mostrando «(sin publicar)» después de que el artículo ya está vivo, los dos PR se apilan: 1. El PR padre del publicador contiene URL, `medios`, imagen y LuaLaTeX, pero no `build/index.json`; permanece en borrador. 2. La misma sesión, ahora declarada en rol proveedor, crea una rama desde la rama del publicador, ejecuta `indice.py`, actualiza índice, mapa y metadatos globales, y abre un **PR hijo de regeneración cuya base es la rama del publicador**, no `main`. Antes de regenerar, compara la rama padre con `origin/main`. Si el padre se quedó atrás, fusiona `origin/main` en la rama hija con un **merge real, no squash**; verifica que sobrevivan tanto la URL/imagen del padre como los datos vigentes del banco y solo entonces ejecuta `indice.py`. Antes de commitear el hijo debe pasar `python scripts/verificar_publicacion.py` para la entidad publicada. La regeneración se hace después de congelar **todos** los campos del padre: tanto `url` como `medios` alimentan `build/index.json`. Si el publicador corrige una atribución, licencia o `fuente_url` después de crear o fusionar el hijo, ese hijo queda obsoleto y debe regenerarse otra vez antes de cerrar el padre. 3. Cuando la CI del PR hijo pasa, el rol proveedor lo fusiona en la rama del publicador. El PR padre incorpora así el índice regenerado sin mezclar los commits ni los límites de cada fase. 4. Se vuelve a ejecutar la CI del PR padre. Solo entonces se marca listo y se fusiona a `main`. 5. El flujo de publicación no está completo hasta regenerar y verificar las **ocho salidas** posteriores a Ghost: ```bash python scripts/build.py python scripts/indice.py python scripts/epub.py --salida build/atlas.epub --solo-publicados python scripts/libro.py --salida build/libro.pdf --solo-publicados python scripts/paquete_latex.py --salida build/biosemiotics-latex.zip python scripts/verificar_publicacion.py --id --url \ --verificar-derivados --epub build/atlas.epub ``` La verificación de derivados va **al final**: comprueba `build/quarto/libro.tex`, que solo existe una vez renderizado el PDF. Las salidas son `index.json`, `atlas-inject.html`, `jsonld/`, el XML experimental de `jats/`, el proyecto Quarto `build/quarto/` (con `libro.tex` dentro), `libro.pdf`, `atlas.epub` y el paquete `biosemiotics-latex.zip`. Se versionan **dos**: `build/index.json` y `build/atlas-inject.html` —ambos los escribe `indice.py` y CI comprueba la deriva de los dos—; las demás se regeneran. La verificación compara la URL en los cuatro derivados web/metadatos y exige que cada imagen publicada sea la declarada en `archivo_local`, tanto en LaTeX como dentro del contenedor EPUB. El ZIP debe conservar `libro.tex`, `refs.bib` y las imágenes del proyecto para recompilar sin depender del checkout original. El workflow `epub.yml` reproduce este contrato en cada PR que toca contenido, imágenes o generadores y vuelve a generarlo automáticamente al fusionarse en `main`; no requiere una ejecución manual. Antes de fusionar el padre, su CI debe mostrar en verde **todas** las variantes del job de integridad (Python 3.9 y 3.13) y el job de citas. El piso 3.9 no es solo documentación: evita fusionar sintaxis que funcione en CI moderna pero no en el runtime local compartido. Esta secuencia mantiene un único dueño de `build/index.json` en cada fase, evita conflictos de responsabilidad y reduce a **cero** la ventana visible de desincronización en `main`. La misma sesión debe completar ambos PR; no puede declarar terminado el flujo únicamente porque Ghost ya publicó. ## Fase proveedora — separada de la fase publicadora Lo que sigue documenta el rol proveedor del índice. La misma sesión solo queda autorizada a ejercerlo después de congelar el PR padre y crear una rama hija; no autoriza al rol publicador activo a ejecutar `indice.py` ni a tocar `build/index.json`. En la fase proveedora, `build.py` NO regenera `index.json` — eso lo hace `indice.py`. Si el proveedor modifica un `.qmd`, debe correr ambos scripts antes de commitear para no servir entradas obsoletas. Cuando recibe un PR de publicación, el proveedor crea el PR hijo de regeneración descrito arriba y lo fusiona sobre la rama del publicador antes de que el PR padre llegue a `main`. **Verifica antes de commitear.** Después de `indice.py`, confirma que la ficha quedó como esperas: ```bash python -c "import json; d=json.load(open('build/index.json',encoding='utf-8'))['fichas']; print([f['url'] for f in d if f['id']==''])" ``` El contador `⚠ N sin url` es solo informativo: puede quedarse igual si otra rama añade simultáneamente una ficha sin publicar. La autoridad es `verificar_publicacion.py`, que compara la entidad concreta con el índice. **Distinción crítica de URLs.** Hay dos clases y NO son lo mismo: - **URLs de artículos** (campo `url` de cada `.qmd`, y las del JSON-LD) → `www.biosemiotics.net`. - **URL del índice** que consume el buscador → **siempre desde GitHub, JAMÁS desde `biosemiotics.net`.** El `index.json` vive en el repositorio, no en el sitio. Apuntar el buscador al dominio lo rompe. **El índice se pide con dos fuentes, primario y respaldo** (`var IDX` e `IDX2` en atlas-inject.html): | | URL | Caché | |---|---|---| | **Primario** | `raw.githubusercontent.com/alcyedmundo281/biosemiotics/main/build/index.json` | 5 min | | **Respaldo** | `cdn.jsdelivr.net/gh/alcyedmundo281/biosemiotics@main/build/index.json` | 12 h | Las dos sirven **el mismo archivo del mismo repositorio**. El buscador pide la primaria con `cache: 'no-cache'` y solo cae a la segunda si falla (rate-limit de GitHub, corte). **Por qué este diseño, y no solo jsDelivr:** jsDelivr cachea las rutas de RAMA (`@main`) durante 12 horas (`s-maxage=43200`). Purgar no siempre basta, y está comprobado que **ni `@latest` ni un `?v=` la esquivan** —jsDelivr ignora los query strings, y `@latest` resuelve al último *tag*, que congelaría el atlas en el release en vez de seguir a `main`. El resultado era publicar un signo y que el atlas siguiera diciendo "(sin publicar)" medio día. Por eso raw va primero: se actualiza en 5 minutos. Las dos URLs (primaria raw, respaldo jsDelivr) son **constantes fijas en `indice.py`** (`URL_PRIMARIA` / `URL_RESPALDO`); NO se pasan por argumento. El comando es `python scripts/indice.py .` a secas —si le pasas una URL, falla con `unrecognized arguments` en vez de ignorarla en silencio. `indice.py` imprime las dos al terminar; verifícalas ahí. Si algún día hay que reconfigurarlas, será una bandera explícita, no un positional. **Cuándo hay que repegar `atlas-inject.html` en Ghost:** solo si cambia la estructura del buscador (diseño, facetas, lógica de fetch). Para publicar contenido NO hace falta —basta el ciclo de arriba. **El Reto sigue la misma regla.** La página `/reto/` de Ghost ejecuta pegado el script de `artefactos/reto.html`; si ese archivo cambia, hay que repegarlo. `verificar_publicacion.py --id --comprobar-web` recorre artículo → Reto enfocado → índice público y avisa si la página del Reto quedó con otra versión o si el índice aún no trae la ficha. La lógica del Reto la prueba `tests/test_reto.py`, que ejecuta ese mismo script en Node. ## Flujo del proveedor: ramas y Pull Requests **`main` está protegida: no se le hace push directo.** Todo cambio entra por un Pull Request que la CI debe aprobar antes de fusionar. Los dos roles se representan con ramas y PR distintos, aunque los ejecute la misma sesión. El ciclo, para cualquier cambio: ```bash git switch -c # p. ej. signo-neumotorax, fix-url-ecogenicidad # ...editas .qmd, corres build.py + indice.py, commiteas... git push -u origin gh pr create --fill # abre el PR # espera a que la CI pase (gh pr checks --watch) gh pr merge --merge --delete-branch # merge real; nunca squash git switch main # abandona la rama ya fusionada git pull --ff-only # trae el merge commit de GitHub git fetch --prune # elimina referencias origin/* ya borradas gh pr list --state open # confirma que no quedan PR pendientes git branch -vv # detecta ramas locales cuyo remoto está gone git worktree list # no borres ramas ocupadas por un worktree git status -sb # main debe coincidir con origin/main ``` **Los PR hacia `main` se fusionan con merge real, nunca con squash.** Así los commits revisados de la rama quedan como ancestros de `main`; después de actualizar el repositorio, Git no los presenta como trabajo pendiente ni obliga a reconciliar una historia equivalente con SHA distintos. La misma regla rige los PR hijo apilados. No termines el flujo desde la rama de trabajo: vuelve siempre a `main`, actualiza, poda y verifica. Una rama todavía asociada a un worktree no se borra a ciegas; primero identifica ese worktree y conserva cualquier cambio que no pertenezca al PR. Reglas: - **Una rama por unidad de trabajo** (un signo, un arreglo). PRs chicos se revisan y se fusionan sin fricción. - **No fusiones con la CI en rojo.** El job *Integridad* es obligatorio: si falla, algo real está mal (índice sin regenerar, arista rota, caso sin consentimiento). - **Trabajo concurrente excepcional:** cada unidad permanece en su rama. Si dos ramas tocan lo mismo, la que fusiona segundo actualiza desde `main` y resuelve en su rama —nunca en `main`. Y **nunca fusiones contenido con citas sin re-verificar** que sobrevivieron intactas (`python scripts/verificar_citas.py`). ## Higiene de Git Después de cada tarea significativa: `git add`, `git commit` con mensaje claro. Es el punto de restauración. Con un agente editando de forma autónoma, commitear seguido no es opcional — es la red de seguridad. El `push` va a **tu rama**, no a `main` (ver arriba). ### Worktree único y ramas por rol El flujo normal usa exclusivamente el worktree principal. Publicador y proveedor se separan mediante ramas/PR apilados y cambios secuenciales de rama, no mediante worktrees adicionales. Antes y después de cada tarea se ejecuta `git worktree list`: cualquier worktree extra debe identificarse y retirarse solo con autorización explícita, después de comprobar que no contiene cambios sin guardar. Nunca se usa `--force` ni se borra manualmente su directorio. Las ramas remotas ya fusionadas sí puede eliminarlas quien fusiona el PR. ## Lo que NO debes hacer - No reescribir arquitectura que ya funciona "para mejorarla" sin que Alcy lo pida. - No saltar de oleada en el mapa maestro por iniciativa propia. - No publicar, borrar, ni hacer push destructivo sin confirmación. - No completar contenido clínico "de tu conocimiento general" — este atlas se apoya en fuentes verificadas y en el criterio de un médico, no en lo que un modelo recuerda. ## El norte Cada cifra verificada. Cada signo con sus límites. El orden por oleada mantiene vivo el mensaje del proyecto: *empezar POCUS es más fácil de lo que te dijeron.* La arquitectura ya está hecha; tu trabajo es hacerla crecer sin degradar su rigor.