Communitygithub.com

h8nc4y/fusión-idempotente-de-sección-markdown

Habilidad para Claude Code y Codex: reemplazar o agregar una sección Markdown de forma idempotente, sin corromper bloques de código. Escaneo de encabezados consciente de fences, límites con marcadores fijos y una receta de verificación de aplicar dos veces con diferencia cero, con una implementación de referencia probada.

¿Qué es fusión-idempotente-de-sección-markdown?

Esta habilidad proporciona un procedimiento robusto para que agentes de IA (como Claude Code o Codex) mantengan una sección canónica en un documento Markdown de forma idempotente, reemplazándola si existe o agregándola si no, con la garantía de que aplicar la operación dos veces produce una salida idéntica byte a byte. Resuelve un problema común: los escaneos ingenuos de encabezados con `^##` fallan cuando hay encabezados literales dentro de bloques de código delimitados (fences) o cuando hay subencabezados de nivel inferior (`###`), corrompiendo el documento. La habilidad ofrece dos métodos de límite seguro: un escaneo consciente de regiones literales (que sigue las reglas de CommonMark para fences, frontmatter y bloques HTML) y el uso de marcadores fijos con comentarios HTML. Además, impone invariantes estrictos y proporciona una receta de verificación con tres comprobaciones, así como una implementación de referencia en Python sin dependencias externas. Es ideal para automatizar la actualización de secciones en archivos como `README.md`, `CLAUDE.md` o `AGENTS.md` de forma fiable y sin efectos secundarios.

Compatible con✓Claude Code✓Codex CLI~Cursor
npx skills add h8nc4y/markdown-idempotent-section-merge

Preguntar en tu IA favorita

Abre un nuevo chat con esta habilidad de agente ya precargada.

Documentación

Fusión Idempotente de Sección Markdown

Procedimiento para mantener una sección canónica de ## Encabezado en un documento Markdown bajo automatización: reemplazar la sección cuando existe, agregarla cuando no existe, y producir una salida idéntica byte a byte al aplicarse dos veces. La parte no obvia es dónde termina la sección — y el error clásico es confiar en una expresión regular ^## para encontrar ese límite.

Cuándo Usarlo

  • Un agente o script mantiene una sección canónica actualizada en un documento que no posee completamente — README.md, AGENTS.md, CLAUDE.md, un manual, un preámbulo de changelog — bajo la regla de "reemplazar si está presente, agregar si falta".
  • Ves alguno de estos síntomas:
    • La sección mantenida se duplica después de cada ejecución.
    • Una fusión cortó la sección prematuramente en medio de un bloque de código delimitado.
    • Una línea ## ... dentro de un bloque de código se trató como la siguiente sección.
    • Un subencabezado ### terminó el rango de reemplazo antes de tiempo.
    • El actualizador nunca converge: la segunda ejecución sigue cambiando el archivo.
    • Después de una fusión, todo lo que está debajo de algún punto se renderiza como un bloque de código gigante (un delimitador de bloque sobrante se reabre como un nuevo bloque).
  • Estás leyendo o contando secciones con patrones tipo ^##, no solo escribiéndolas — la extracción y verificación caen en la misma trampa.

La Trampa: Encabezados Dentro de Bloques de Código

La implementación ingenua toma el rango de reemplazo como "la línea ## X hasta la línea anterior a la siguiente coincidencia de ^##". Esto falla de dos maneras.

Trampa 1 — literales dentro de fences. Los cuerpos de las secciones contienen legítimamente líneas ## ... dentro de bloques de código delimitados: una plantilla de informe, un documento de ejemplo, un diff citado. Un escaneo ciego a los fences lee erróneamente la primera línea de ese tipo como el límite de la siguiente sección y corta el rango allí:

## Notas de automatización

El bot actualiza esta sección.

```text
## Informe semanal        <- el escaneo ingenuo de ^## se detiene aquí, dentro del fence
- aspectos destacados:
- riesgos:
```

Mantén la plantilla dentro del fence para que no se convierta en un encabezado real.

## Licencia              <- la siguiente sección real

Reemplazar hasta la línea dentro del fence deja una cola obsoleta. La corrupción medida (ver el fixture de prueba de la trampa) es peor que una simple duplicación:

  • El literal ## Informe semanal dentro del fence se queda fuera de cualquier fence y se renderiza como un encabezado real, que parece duplicado.
  • El delimitador de cierre del bloque antiguo (la línea ``` sin contenido) también se queda, se reabre como un nuevo fence, y se traga todo lo que sigue — la siguiente sección ## Licencia deja de ser un encabezado.
  • La fusión no es idempotente: cada ejecución vuelve a coincidir con el encabezado, corta en una línea dentro del fence otra vez, y agranda el archivo otra vez.

Trampa 2 — subencabezados ###. Una expresión regular ^## desnuda también coincide con ###, ####, etc. Si la sección contiene subencabezados, el rango termina en la primera línea ### ... y las subsecciones antiguas sobreviven después de las nuevas — contenido duplicado debajo del punto de fusión.

Las mismas dos trampas se aplican a leer una sección (extraer su contenido) y a verificar una (grep -c '^## X' puede tanto sobrecontar literales dentro de fences como ser engañado por ellos).

Límites Seguros: Dos Métodos

Método 1 — escaneo de encabezados consciente de regiones literales

Rastrear el estado de región literal mientras se escanean líneas, y solo tratar una línea como encabezado o límite cuando está fuera del frontmatter, los fences y los bloques HTML sin procesar admitidos:

  • Un fence se abre en una línea cuyos primeros caracteres no espaciales (como máximo 3 espacios al inicio) son una secuencia de 3+ acentos graves o 3+ virgulillas.
  • Se cierra solo en una línea con una secuencia del mismo carácter, al menos tan larga, sin nada más en la línea. Un fence no cerrado llega hasta el final del documento (comportamiento CommonMark).
  • Mientras se está dentro de un fence, nada es un encabezado y nada es un límite.
  • Antes del escaneo de fences, se excluye un bloque de frontmatter inicial de línea exacta: YAML se abre con --- y se cierra con --- exacto o ...; TOML se abre y cierra con +++ exacto. Una línea con apariencia de encabezado dentro de ese bloque es contenido de metadatos/comentario, no un límite de sección Markdown. Un apertura sin su cierre exacto falla de forma segura antes de cualquier escritura.
  • En el nivel superior, las columnas 0–3 de los bloques HTML sin procesar CommonMark 0.31.2 de tipos 1–7 también se excluyen: etiquetas de contenido literal (pre/script/style/textarea), comentarios, instrucciones de procesamiento, declaraciones, CDATA, etiquetas de bloque listadas y líneas de solo etiqueta completas. Los tipos 6/7 terminan antes de una línea en blanco o en EOF; el tipo 7 no puede interrumpir un párrafo. La gramática de etiquetas y atributos sigue siendo solo ASCII incluso bajo coincidencia sin distinción de mayúsculas/minúsculas; los homoglifos Unicode de pliegue de mayúsculas son Markdown ordinario. El estado de fence y HTML comparte un único escáner exclusivo, por lo que los delimitadores dentro de la otra región se mantienen como datos literales.
  • Esta referencia mutante es intencionalmente más estricta que CommonMark en un EOF ambiguo: un apertura reconocida de tipo 1–5 sin su condición de cierre explícita falla de forma segura. Una posible apertura de tipo 7 después de un contexto de contenedor/sangría no compatible también falla de forma segura en lugar de adivinar. La misma regla cubre una posible definición de referencia de enlace seguida de ===, donde un analizador parcial no puede probar si el setext terminó el párrafo. Las posibles etiquetas de enlace se rastrean a través de líneas con manejo de corchetes consciente de escapes; un ] sin escapar sin dos puntos inmediatos libera el estado de posible definición. La forma simple de definición sin línea en blanco más etiqueta sigue siendo HTML en línea.
  • El límite de la sección es el siguiente encabezado de nivel 1 o 2. La forma popular es ^##[^#] (excluir ###); la referencia lo refuerza a ^ {0,3}#{1,2}([ \t]|$), que además trata un H1 como un límite (un límite de parte nunca debe ser tragado en un reemplazo), acepta la sangría de 1 a 3 espacios que CommonMark permite y aplica la regla de espacio de CommonMark para que una línea de párrafo de estilo ##hashtag no sea un límite falso. ### sigue sin ser un límite de todos modos.

Usa este método cuando el formato del documento no se puede cambiar, o cuando la herramienta debe funcionar en Markdown arbitrario.

Método 2 — marcadores fijos

Envuelve la sección mantenida en líneas centinela únicas — los comentarios HTML son invisibles en Markdown renderizado:

<!-- managed-section:begin automation-notes -->
## Notas de automatización

...cuerpo, plantillas dentro de fences bienvenidas...
<!-- managed-section:end automation-notes -->

Reemplaza todo entre las líneas exactas de marcador de inicio/fin; agrega todo el bloque marcado cuando el marcador de inicio está ausente. El escaneo de encabezados ya no es parte de la búsqueda de rangos.

Usa este método cuando posees el formato del documento: es el límite más robusto (sobrevive a renombrados de encabezados y cualquier contenido del cuerpo), a costa de colocar marcadores. Mantén las cadenas de marcador únicas por sección y mantén la invariante de H2 único a continuación incluso aquí — los marcadores protegen el rango, no la estructura de encabezados del documento. Una nota de honestidad: una copia literal de la línea de marcador dentro de un fence aún confundiría una búsqueda de marcador ciega a fences, así que mantén el texto del marcador fuera de ejemplos dentro de fences o encuentra marcadores con el mismo escaneo consciente de fences.

Invariantes

Aplica estas antes y después de cada fusión; hacen que "reemplazar o agregar" esté bien definido:

  1. El bloque comienza con su propia línea ## Encabezado sin formato — usa exactamente un espacio ASCII entre la ## de apertura y el contenido no vacío (o exactamente ## para un H2 vacío). Los espacios/tabuladores separadores adicionales y una secuencia de cierre con hash de CommonMark no son canónicos aquí. El encabezado es parte del contenido fusionado, no una configuración que pueda desviarse de él.
  2. Exactamente un encabezado de nivel H1/H2 dentro del bloque — su propia primera línea ATX (recuento consciente de regiones literales). Un bloque con un segundo ATX o setext H1/H2 absorbería o dividiría silenciosamente una sección en una ejecución posterior. Rechaza posibles encabezados setext antes de la primera escritura para que agregar y reemplazar usen el mismo límite de validación. Los literales de encabezado dentro de fences o HTML sin procesar dentro del bloque están bien — no cuentan.
  3. Como máximo una copia inequívoca del encabezado en el destino (fuera de regiones literales). Si el documento ya contiene duplicados o un separador / espacio 1–3 / alias de cierre hash del H2 gestionado, detente e informa en lugar de "arreglar" una forma y dejar otra.
  4. Una forma de separador canónica. Almacena el bloque sin líneas en blanco finales; escribe exactamente una línea en blanco entre el bloque y la sección siguiente. El rango de lectura y la forma de escritura convergen entonces a los mismos bytes, que es lo que hace que la segunda ejecución sea un no-op.
  5. Sin fences no cerrados, en el destino o en el bloque. CommonMark ejecuta un fence no cerrado hasta el final del documento, por lo que en un destino extiende silenciosamente la sección hasta EOF (un reemplazo reescribiría toda la cola visualmente tragada), y en un bloque se tragaría lo que sigue a la sección fusionada. Ambas son entradas mal formadas: detente e informa.
  6. Sin encabezados setext dentro del bloque canónico o del intervalo reemplazado. Un subrayado === o --- directamente debajo del texto de un párrafo es un encabezado real que un escaneo línea por línea de ^# no puede ver como un límite. Aceptar uno en un bloque recién agregado haría que la siguiente ejecución falle en lugar de converger; reemplazar a través de uno eliminaría un límite de sección. Detente antes de escribir en ambos casos (convierte el encabezado a forma ATX, o usa marcadores fijos). Ver docs/commonmark-setext-heading-contract.md.
  7. Sin frontmatter inicial no cerrado. Cuando la primera línea es exactamente --- (YAML) o +++ (TOML), ignora sus líneas con apariencia de encabezado solo hasta el primer cierre exacto coincidente. Si no existe un cierre, detente en lugar de adivinar si la apertura era metadatos o una ruptura temática ordinaria de Markdown.
  8. Sin HTML sin procesar de final explícito ambiguo. CommonMark permite que los tipos 1–5 se ejecuten hasta EOF cuando falta su condición de final, pero agregar un H2 allí lo ocultaría dentro del HTML sin procesar y rompería la convergencia. Requiere la condición de final explícita tanto en el destino como en el bloque. Para el tipo 7, preserva su regla de no interrupción de párrafo y falla de forma segura cuando un contexto de contenedor/sangría no compatible o de posible referencia más setext, incluyendo una etiqueta de enlace de varias líneas, hace que esa decisión sea ambigua.
  9. Espacio en blanco de bloque solo ASCII. Las líneas en blanco, el recorte de encabezados ATX y las secuencias de cierre, el contexto setext y los cerradores de fence usan espacio/tabulador ASCII exactamente como CommonMark 0.31.2 especifica. NBSP, EM SPACE, avance de página, tabulación vertical y otros espacios en blanco Unicode permanecen como contenido en lugar de ser eliminados silenciosamente. Ver docs/commonmark-ascii-whitespace-contract.md.

Receta de Verificación

Ejecuta las tres comprobaciones después de una fusión; juntas detectan errores de límite, secciones duplicadas y ediciones colaterales:

  1. Aplicar-dos-veces-diferencia-cero. Aplica la misma fusión dos veces; después de la segunda ejecución, git diff debe estar vacío (o comparar bytes antes/después). Para una sección que se encuentra al final del archivo, una comprobación equivalente es que el bloque es igual a toda la sección final del archivo.

    python scripts/merge_section.py target.md section.md
    python scripts/merge_section.py target.md section.md   # segunda ejecución
    git diff --exit-code target.md   # después de confirmar la primera ejecución: vacío
    

    El modo --check de la implementación de referencia codifica la misma idea con códigos de salida de estilo diff (0 = canónico, 1 = una fusión cambiaría el archivo), que CI puede llamar directamente.

  2. Recuento de ocurrencias de encabezado = 1. Cuenta las líneas iguales al encabezado con un escaneo consciente de fences. Un grep -c '^## Notas de automatización$' está sujeto a la misma trampa de la que trata esta habilidad — aceptable solo cuando sabes que el cuerpo no contiene una copia dentro de un fence de la línea de encabezado exacta.

  3. git diff --stat toca exactamente un archivo — el destino. Cualquier otra cosa significa que la automatización escribió donde no debía.

Implementación de Referencia

scripts/merge_section.py implementa el Método 1 más cada invariante anterior en Python 3 sin dependencias (biblioteca estándar solamente). Python se eligió sobre un shell por una razón que importa aquí: control explícito a nivel de bytes de nuevas líneas y BOM hace que aplicar-dos-veces-diferencia-cero sea demostrable, donde las tuberías de texto de shell tienden a normalizar los finales de línea sin que te des cuenta. El algoritmo se porta directamente a cualquier lenguaje.

python scripts/merge_section.py TARGET.md SECTION.md            # fusionar en el lugar
python scripts/merge_section.py TARGET.md SECTION.md --check   # comprobación de desviación

SECTION.md es el bloque canónico: la primera línea es exactamente ## Encabezado, con un espacio ASCII antes de contenido no vacío, o exactamente ## para un H2 vacío. El estilo LF/CRLF y el BOM UTF-8 del destino se preservan; un destino faltante se crea (agregar en un documento vacío). Los bytes cambiados se vacían a un archivo temporal exclusivo del mismo directorio y se instalan con un reemplazo atómico. El temporal comienza con modo POSIX 0600 o una DACL protegida de Windows SISTEMA/Solo-Dueño, y su identidad, bytes, metadatos y DACL de Windows se vuelven a comprobar antes de la confirmación. Los atributos extendidos POSIX existentes de propietario/grupo/modo/acotados se preservan. Para un destino de Windows existente, ReplaceFileW traslada su DACL documentada, atributos de archivo y flujos nombrados; no se promete preservación exacta de propietario/grupo/SACL. Los destinos faltantes conservan los permisos privados. Tanto el destino como el bloque canónico se leen como instantáneas de archivo ordinario sin seguimiento. Las entradas de enlace simbólico, no regulares y de múltiples enlaces duros se rechazan porque el reemplazo cambiaría su semántica; los puntos de reanálisis de Windows y los destinos de Windows cifrados con EFS se rechazan antes de leer el contenido. La identidad, metadatos y bytes del destino se vuelven a comprobar justo antes de la confirmación. Los cambios completados antes de esa comprobación se detectan, pero la comprobación y el reemplazo son operaciones separadas; serializa externamente a cada escritor cuando un destino existente no debe sufrir una actualización perdida. La creación de destino faltante usa una confirmación sin reemplazo y nunca sobrescribe una creación concurrente. En Windows, ReplaceFileW usa un

...

[truncado para traducción]

Skills relacionados