Si tu pipeline de construcción no puede ejecutarse sin que abras una terminal y escribas make tangle, no tienes un programa literario. Tienes un diario con un compiler adjunto.

Todo el punto de la programación literaria es que la prosa y el código compartan una única fuente de verdad. El archivo Markdown es el artefacto. Todo lo demás — el código fuente ejecutable, la documentación renderizada, los archivos de prueba — es derivado. Los artefactos derivados pertenecen a CI, no a tu memoria de trabajo.

La pregunta no es si CI puede hacer weave y tangle. Por supuesto que puede. Cualquier máquina que ejecute Python y pandoc puede hacerlo. La pregunta es si la estructura de tu repository permite que CI sea dueño de los archivos derivados, o si todavía estás fingiendo que los archivos .py son código fuente cuando en realidad son productos de compilación.

Qué hacen realmente weave y tangle

Donald Knuth llamó a las dos operaciones weave y tangle, y los nombres son tan confusos que la gente evita el tema por completo.

tangle toma el documento narrativo — el archivo .md o .w — y extrae los bloques de código en el orden que espera el compiler. Produce los archivos de código fuente ejecutables. weave hace lo contrario. Toma el mismo documento y produce la documentación legible para humanos, con código pretty-printed, referencias cruzadas y una tabla de contenidos.

Ambas son transformaciones deterministas. Toman una entrada y producen salidas consistentes. Esa es la definición de algo que CI debería manejar.

Por qué la mayoría de los equipos construyen la pipeline al revés

El flujo de trabajo típico de programación literaria se ve así. Escribas algorithm.md. Ejecutas un script local para extraer algorithm.py. Ejecutas pruebas contra algorithm.py. Haces commit de ambos algorithm.md y algorithm.py en Git. Luego abres un pull request.

Esto ya está roto. Tienes dos copias de la misma lógica en control de versiones. Si un revisor sugiere un cambio en algorithm.py, tienes que recordar portarlo a algorithm.md. Si editas algorithm.md y olvidas re-hacer tangle, el archivo .py commiteado está obsoleto. La única fuente de verdad es una ficción.

El flujo de trabajo correcto es más simple. Solo haces commit del archivo .md. CI ejecuta tangle para producir el archivo .py, y luego ejecuta la suite de pruebas contra él. Si las pruebas pasan, CI opcionalmente ejecuta weave para publicar documentación. El archivo .py nunca toca el control de versiones. Es producto de compilación, como un archivo .o o una imagen Docker.

Una pipeline de CI funcional para Markdown-a-Python

Aquí hay un flujo de trabajo completo de GitHub Actions que trata el archivo Markdown como la única fuente de verdad. Guárdalo como .github/workflows/literate.yml:

name: Tangle and Test

on:
  push:
    paths:
      - "**/*.md"
  pull_request:
    paths:
      - "**/*.md"

jobs:
  tangle:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Set up Python
        uses: actions/setup-python@v5
        with:
          python-version: "3.12"

      - name: Tangle code from Markdown
        run: python scripts/tangle.py src/*.md --out build/

      - name: Run tests on extracted code
        run: python -m pytest build/ -v

      - name: Upload generated source as artifact
        uses: actions/upload-artifact@v4
        with:
          name: generated-source
          path: build/

El filtro paths es importante. Este flujo de trabajo solo se ejecuta cuando cambia un archivo Markdown, porque Markdown es la única entrada que importa.

El script tangle.py es el mismo extractor de treinta líneas que ejecutarías localmente. Aquí hay una versión que maneja múltiples archivos y un directorio de salida:

import argparse
import re
import sys
from pathlib import Path


def tangle(md_path: Path, out_dir: Path) -> None:
    content = md_path.read_text()
    files: dict[str, list[str]] = {}

    for block in re.findall(r"```python\n(.*?)```", content, re.DOTALL):
        lines = block.strip().split("\n")

        if lines and lines[0].startswith("# "):
            filename = lines[0][2:].strip()
            code = lines[1:]
        else:
            filename = "module.py"
            code = lines

        files.setdefault(filename, []).extend(code)

    out_dir.mkdir(parents=True, exist_ok=True)
    for filename, code_lines in files.items():
        (out_dir / filename).write_text("\n".join(code_lines) + "\n")


def main() -> None:
    parser = argparse.ArgumentParser(description="Extract code from Markdown files.")
    parser.add_argument("inputs", nargs="+", type=Path, help="Markdown files to process")
    parser.add_argument("--out", type=Path, required=True, help="Output directory")
    args = parser.parse_args()

    for md in args.inputs:
        tangle(md, args.out)


if __name__ == "__main__":
    main()

Ejecútalo localmente para verificar:

$ python scripts/tangle.py src/calculator.md --out build/
$ python -m pytest build/ -v

El trabajo de CI hace exactamente lo mismo. La única diferencia es que CI lo ejecuta en un entorno limpio, por lo que un archivo .py obsoleto escondido en tu directorio de trabajo no puede engañar a la suite de pruebas.

Tejiendo documentación en la misma pipeline

tangle produce código. weave produce documentación. Ambos pueden ejecutarse en CI.

Para weave, necesitas una herramienta que convierta Markdown en un formato presentable. Pandoc es la opción aburrida y confiable. Si quieres referencias cruzadas, resaltado de sintaxis y una tabla de contenidos, una plantilla pequeña te lleva allí:

      - name: Weave documentation
        run: |
          mkdir -p docs/output
          pandoc src/*.md \
            --from markdown \
            --to html5 \
            --standalone \
            --toc \
            --highlight-style=tango \
            --output docs/output/index.html

      - name: Publish to GitHub Pages
        uses: actions/upload-pages-artifact@v3
        with:
          path: docs/output/

El HTML renderizado también es un artefacto derivado. Puedes publicarlo en GitHub Pages, Netlify o un bucket de S3 en cada merge a main. El archivo Markdown permanece canónico. El HTML es lo que ven los lectores.

El compromiso entre hacer commit y generar

Algunos equipos se resisten a esto. Quieren los archivos .py en Git porque facilita git grep y porque la interfaz de revisión de código de GitHub entiende la sintaxis de Python mejor que Markdown.

Son problemas reales, pero son problemas de herramientas, no de arquitectura. Hacer commit de código generado porque tu herramienta de revisión de código es mala es como hacer commit de node_modules porque tu package manager es lento. Funciona, pero acumula deuda.

Si absolutamente necesitas los archivos .py en el repository, genéralos en CI y haz commit de ellos con una cuenta de bot. Este es el patrón:

  1. Un desarrollador hace push de un cambio a algorithm.md.
  2. CI ejecuta tangle.py y produce algorithm.py.
  3. Si el código extraído difiere de lo que está en main, CI abre un pull request con el archivo .py actualizado.
  4. Un humano revisa el diff generado y lo fusiona.

Esto mantiene los archivos generados en control de versiones sin dejar que se desvíen. El bot se convierte en la única fuente de verdad para la salida generada, y el bot solo cambia lo que el archivo Markdown exige.

Cuándo la automatización de CI vale la pena, y cuándo no

El weave y tangle automatizado brillan cuando la prosa es larga, el código es complejo, y varias personas editan el documento. El código de investigación, la documentación de bibliotecas y las implementaciones de algoritmos se benefician de un archivo Markdown canónico que CI valida en cada push.

No vale la pena para scripts pequeños o código de aplicación con mucho boilerplate. Si tu archivo Markdown es dos párrafos y una función de veinte líneas, el overhead de CI, los minutos de runner y el costo del modelo mental superan el problema de sincronización que estás resolviendo. Simplemente escribe un buen docstring.

El otro caso en el que esto falla es cuando tu tooling de lenguaje asume que los archivos .py son primarios. Los debuggers, los perfiladores y las herramientas de cobertura generalmente quieren apuntar al código generado, no al Markdown. Puedes solucionarlo generando archivos localmente antes de depurar, pero eso es fricción. Si tu equipo vive en un debugger, la programación literaria podría no ser adecuada independientemente de cuán buena sea tu pipeline de CI.

FAQ

¿Debería hacer commit de los archivos .py generados o agregarlos a .gitignore?

Agregarlos a .gitignore. Si se generan determinísticamente desde Markdown, son artefactos de compilación. La única excepción es si un sistema externo, como un registry de packages, no puede ejecutar tu paso de tangle. En ese caso, usa el patrón de commit de bot descrito anteriormente.

¿Qué pasa si mi documento literario mezcla varios lenguajes?

El script tangle.py es independiente del lenguaje. Usa # filename.rs para bloques de Rust, # filename.go para bloques de Go, y así sucesivamente. CI ejecuta el mismo script para cada lenguaje. Solo necesitas un paso de prueba por lenguaje en el flujo de trabajo.

¿Funciona esto con chunks de código con nombre como el WEB de Knuth?

Herramientas modernas como Entangled admiten chunks con nombre y tracing de dependencias. Si necesitas ese nivel de expresividad, usa Entangled en CI en lugar de un script personalizado. La estructura de la pipeline es la misma. Solo cambia la herramienta de checkout.

¿Cómo manejo imports que abarcan varios archivos Markdown?

Cada archivo Markdown se convierte en tangle en su propio directorio de salida. Si math.md define add y geometry.md lo importa, o tanglas ambos archivos en el mismo directorio de compilación o estructuras tu código como un package con imports relativos. El trabajo de CI puede tanglar todos los archivos antes de ejecutar las pruebas, así que las dependencias entre archivos se resuelven naturalmente.

Haz que tu archivo Markdown sea el único que importa

Configura la pipeline una vez. Haz commit del archivo .md. Deja que CI maneje el resto. Si una prueba falla, el fallo apunta a la línea de Markdown que produjo el código, no a un archivo generado que no debería existir en el control de versiones de todos modos.

El objetivo de la programación literaria es una única fuente de verdad. Una fuente de verdad que requiere que un humano recuerde un paso manual no es una fuente de verdad. Es una sugerencia.