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.
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.
Una fuente, muchas salidas. El banco de archivos .md es la ÚNICA fuente de verdad. Todo lo demás (build/, el índice, el HTML, el JATS) es derivado y se regenera. NUNCA edites archivos en build/ a mano: el siguiente indice.py los sobrescribe. Si algo está mal en una salida, se arregla en el .md de origen y se recompila.
proyecto-biosemiotics/
├── CLAUDE.md ← este archivo
├── mapa-maestro-biosemiotics.md ← QUÉ escribir y en qué orden (léelo siempre)
├── conceptos/*.md ← el "por qué" (física, artefactos, técnica)
├── signos/*.md ← el "qué hago" (significante→significado→decisión)
├── casos/*.md ← el paciente real
├── scripts/ ← build.py, indice.py, refs.py, nuevo.py, senuelo.py
├── refs.bib ← bibliografía (SOLO desde PubMed vía refs.py)
├── assets/ ← plantillas para nuevo.py
└── build/ ← GENERADO, no versionar salvo index.json
La documentación de referencia (esquema completo, instructivo del artículo) vive en la skill biosemiotics-atlas. Consúltala si necesitas el detalle de un campo.
build/libro.tex)El compilador correcto es LuaLaTeX, no pdflatex. El banco escribe umbrales y decisiones con símbolos Unicode estructurales (≥, →, ±) porque así se leen en la clínica — parchearlos uno por uno no escala. El preámbulo que genera build_latex() usa fontspec (sin inputenc/fontenc, que son cosas de pdflatex) y fija \setmainfont{FreeSerif}, la única fuente disponible que cubre esos glifos sin fallback silencioso.
Paquetes LaTeX requeridos (además de lo básico de book): fontspec, babel (spanish), biblatex+biber, y para las figuras de los signos con imagen: graphicx, float, adjustbox. En Debian/Ubuntu, texlive-latex-recommended + texlive-latex-extra + texlive-lang-spanish + texlive-luatex + biber cubren todo. Si falta adjustbox.sty, la compilación aborta de inmediato con File 'adjustbox.sty' not found — no es un error del banco.
cd build
lualatex -interaction=nonstopmode libro.tex
biber libro
lualatex -interaction=nonstopmode libro.tex
lualatex -interaction=nonstopmode libro.tex # segunda pasada: referencias cruzadas
Si compilas con pdflatex vas a ver Missing character o Unicode character not set up for use with LaTeX en cuanto el texto traiga ≥/→/± — no es un error del banco, es el compilador equivocado.
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 piso soportado del repositorio es Python 3.9. El generador requiere Python
3.9 o posterior, PyYAML y 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 build.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: valida con EPUBCheck, compila el PDF con
LuaLaTeX/Biber 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.
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.
git status y reporta el estado. Si hay cambios sin commitear, avísalo antes de empezar.mapa-maestro-biosemiotics.md y di qué signo toca según la oleada (no saltes de oleada sin que Alcy lo pida).python scripts/build.py y reporta las alertas actuales (qué falta: abstracts, refs, urls).sistema, organo, nivel, oleada. No inventes estos valores — están definidos en la taxonomía del mapa.python scripts/nuevo.py signo <id> "<título>" o partiendo de la plantilla.## LITERALES, que el JATS mapea automáticamente). Registro: permiso para el principiante, frases cortas, español claro.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.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.python scripts/build.py. No continúes con errores.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.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.consentimiento: obtenido, el caso no se publica. Verifica de-identificación: sin DICOM metadata, sin rostro, sin identificadores, sin señalética institucional.build/ a mano. Regenéralo.Una misma sesión conserva los dos roles del ciclo, pero nunca los mezcla en un mismo paso ni en un mismo commit:
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.
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 .md
ya creada y validada durante la fase proveedora, modificar solamente url y
medios; puede añadir el archivo licenciado a assets/img/, ejecutar
build.py, verificar build/libro.tex con LuaLaTeX 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.
Antes de abrir Ghost:
python scripts/auditar_pegado_ghost.py \
--canon build/ghost/<carpeta>/<archivo>.md
url, medios, assets/img/ y las salidas LuaLaTeX correspondientes.Usa exclusivamente build/ghost/<carpeta>/<archivo>.md como cuerpo. No lo
regeneres. Antes de tocar Ghost, guarda su huella esperada:
python scripts/auditar_pegado_ghost.py --canon build/ghost/<carpeta>/<archivo>.md
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.
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/libro.tex; compila con LuaLaTeX cuando el entorno lo permita.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.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:
python scripts/auditar_pegado_ghost.py \
--canon build/ghost/<carpeta>/<archivo>.md \
--captura <texto-visible-del-editor.txt> \
--pie "<pie observado>" --pie-esperado "<atribución canónica>"
Published o
Published and sent) y copia la URL pública definitiva. Nunca uses la URL
del editor (/ghost/#/...) ni una vista previa (/p/...).url de la ficha existente. No
cambies ningún otro campo salvo medios.build.py, valida que build/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.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.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.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.
Para que el buscador nunca quede mostrando «(sin publicar)» después de que el artículo ya está vivo, los dos PR se apilan:
medios, imagen y LuaLaTeX, pero
no build/index.json; permanece en borrador.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.Se vuelve a ejecutar la CI del PR padre. Solo entonces se marca listo y se
fusiona a main.
El flujo de publicación no está completo hasta regenerar y verificar las ocho salidas posteriores a Ghost:
python scripts/build.py
python scripts/indice.py .
python scripts/verificar_publicacion.py --id <id> --url <url> \
--verificar-derivados
python scripts/epub.py --salida build/atlas.epub --solo-publicados
python scripts/verificar_publicacion.py --id <id> --url <url> \
--verificar-derivados --epub build/atlas.epub
python scripts/paquete_latex.py --salida build/biosemiotics-latex.zip
cd build
lualatex -halt-on-error -interaction=nonstopmode libro.tex
biber libro
lualatex -halt-on-error -interaction=nonstopmode libro.tex
lualatex -halt-on-error -interaction=nonstopmode libro.tex
Las salidas son index.json, atlas-inject.html, jsonld/, jats/,
libro.tex, libro.pdf (LuaLaTeX), atlas.epub y el paquete
biosemiotics-latex.zip. Solo
build/index.json se versiona; las otras 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 build/libro.tex,
refs.bib y el árbol completo assets/ 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ó.
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 .md, 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:
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']=='<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:
url de cada .md, y las del JSON-LD) → www.biosemiotics.net.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=<timestamp> 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.
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:
git switch -c <rama-descriptiva> # p. ej. signo-neumotorax, fix-url-ecogenicidad
# ...editas .md, corres build.py + indice.py, commiteas...
git push -u origin <rama-descriptiva>
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:
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).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).
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.
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.