Communitygithub.com

h8nc4y/fusion-idempotente-de-sections-markdown

Compétence pour Claude Code et Codex : remplacer ou ajouter de manière idempotente une section Markdown sans corrompre les blocs de code — détection de titres avec conscience des blocs de code, délimiteurs fixes, et une recette de vérification appliquer-deux-fois-diff-zéro, avec une implémentation de référence testée.

Qu'est-ce que fusion-idempotente-de-sections-markdown ?

Cette compétence résout un problème récurrent dans la manipulation automatisée de fichiers Markdown : remplacer ou ajouter une section identifiée par un titre `##` tout en garantissant que l'opération est idempotente (deux applications produisent le même résultat) et ne corrompt pas le document. Le piège classique est d'utiliser une expression régulière `^##` naïve pour trouver la fin de la section, ce qui échoue lorsque le contenu inclut des blocs de code contenant des lignes `##` ou des sous-titres `###`. La compétence propose deux méthodes robustes : une méthode avancée qui suit l'état des blocs de code et des blocs HTML selon la spécification CommonMark, et une méthode simple utilisant des marqueurs HTML fixes. Elle inclut une implémentation de référence en Python sans dépendances externes, une recette de vérification en trois étapes, et des invariants stricts qui garantissent que la fusion est bien définie et reproductible. Cette compétence est particulièrement utile pour les agents comme Claude Code ou Codex qui doivent maintenir des sections dans des fichiers comme `README.md`, `AGENTS.md` ou `CLAUDE.md` sans risquer de dupliquer, tronquer ou corrompre le document.

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

Demander à votre IA préférée

Ouvre une nouvelle conversation avec cette compétence d'agent déjà préchargée.

Documentation

Fusion Idempotente de Section Markdown

Procédure pour maintenir une section canonique ## Titre dans un document Markdown sous automatisation : remplacer la section lorsqu'elle existe, l'ajouter lorsqu'elle n'existe pas, et produire une sortie identique en octets lorsqu'elle est appliquée deux fois. La partie non évidente est l'endroit où la section se termine — et l'erreur classique consiste à faire confiance à une expression régulière ^## nue pour trouver cette limite.

Quand l'utiliser

  • Un agent ou un script maintient une section canonique à jour dans un document dont il n'est pas le propriétaire exclusif — README.md, AGENTS.md, CLAUDE.md, un manuel, un préambule de journal des modifications — selon la règle "remplacer si présent, ajouter si absent".
  • Vous observez l'un de ces symptômes :
    • La section maintenue est dupliquée après chaque exécution.
    • Une fusion a tronqué la section au milieu d'un bloc de code délimité.
    • Une ligne ## ... à l'intérieur d'un bloc de code a été traitée comme la section suivante.
    • Un sous-titre ### a mis fin prématurément à la plage de remplacement.
    • Le script de mise à jour ne converge jamais : la deuxième exécution modifie encore le fichier.
    • Après une fusion, tout ce qui se trouve en dessous d'un certain point s'affiche comme un bloc de code géant (un délimiteur de bloc restant a rouvert un nouveau bloc).
  • Vous lisez ou comptez des sections avec des motifs de style ^##, pas seulement en écrivant — l'extraction et la vérification tombent dans le même piège.

Le piège : les titres à l'intérieur des blocs de code

L'implémentation naïve considère la plage de remplacement comme "la ligne ## X jusqu'à la ligne précédant la prochaine correspondance ^##". Elle échoue de deux manières.

Piège 1 — littéraux dans les blocs de code. Les corps de sections contiennent légitimement des lignes ## ... à l'intérieur de blocs de code : un modèle de rapport, un document d'exemple, un diff cité. Un scan aveugle aux blocs de code interprète la première ligne de ce type comme la limite de la section suivante et tronque la plage :

## Notes d'automatisation

Le robot actualise cette section.

```text
## Rapport hebdomadaire        <- le scan ^## naïf s'arrête ici, à l'intérieur du bloc
- points saillants :
- risques :
```

Gardez le modèle dans un bloc pour qu'il ne devienne pas un vrai titre.

## Licence              <- la vraie section suivante

Remplacer jusqu'à la ligne du bloc laisse une queue obsolète. La corruption mesurée (voir le scénario de test du piège) est pire qu'une simple duplication :

  • Le littéral ## Rapport hebdomadaire du bloc est laissé en dehors de tout bloc et s'affiche comme un vrai titre, ressemblant à un doublon.
  • Le délimiteur de fermeture de l'ancien bloc (la ligne nue ```) est également laissé, se rouvre en nouveau bloc, et engloutit tout ce qui le suit — la section ## Licence suivante cesse d'être un titre.
  • La fusion n'est pas idempotente : chaque exécution correspond à nouveau au titre, coupe à une ligne de bloc à nouveau, et agrandit le fichier à nouveau.

Piège 2 — sous-titres ###. Une expression régulière ^## nue correspond également à ###, ####, et ainsi de suite. Si la section contient des sous-titres, la plage se termine à la première ligne ### ... et les sous-sections anciennes survivent après les nouvelles — contenu dupliqué en dessous du point de fusion.

Les deux mêmes pièges s'appliquent à la lecture d'une section (extraction de son contenu) et à la vérification (grep -c '^## X' peut à la fois sur-compter les littéraux dans les blocs de code et être leurré par eux).

Limites sûres : deux méthodes

Méthode 1 — scan de titres avec conscience des régions littérales

Suivre l'état des régions littérales lors du scan des lignes, et ne traiter une ligne comme titre ou limite que lorsqu'elle est en dehors du frontmatter, des blocs de code et des blocs HTML bruts supportés :

  • Un bloc de code s'ouvre sur une ligne dont les premiers caractères non espaces (au maximum 3 espaces en début de ligne) sont une série de 3+ backticks ou 3+ tildes.
  • Il se ferme uniquement sur une ligne avec une série du même caractère, au moins aussi longue, sans rien d'autre sur la ligne. Un bloc de code non fermé s'étend jusqu'à la fin du document (comportement CommonMark).
  • Tant qu'on est à l'intérieur d'un bloc, rien n'est un titre et rien n'est une limite.
  • Avant le scan des blocs, un bloc de frontmatter en début de ligne exacte est exclu : YAML s'ouvre avec --- et se ferme avec --- ou ... exact ; TOML s'ouvre et se ferme avec +++ exact. Une ligne ressemblant à un titre à l'intérieur de ce bloc est un contenu de métadonnées/commentaire, pas une limite de section Markdown. Un ouvreur sans son fermeur exact échoue en échec avant toute écriture.
  • Au niveau supérieur, les colonnes 0–3 des blocs HTML bruts CommonMark 0.31.2 de types 1 à 7 sont également exclus : les balises de contenu littéral (pre/script/style/textarea), les commentaires, les instructions de traitement, les déclarations, CDATA, les balises de bloc listées, et les lignes de balises complètes. Les types 6/7 se terminent avant une ligne vide ou à la fin du fichier ; le type 7 ne peut pas interrompre un paragraphe. La grammaire des balises et attributs reste en ASCII uniquement même sous correspondance insensible à la casse ; les caractères Unicode qui imitent la casse restent du Markdown ordinaire. L'état des blocs de code et HTML partage un seul scanner exclusif, de sorte que les délimiteurs à l'intérieur de l'autre région restent des données littérales.
  • Cette référence mutante est intentionnellement plus stricte que CommonMark pour une fin de fichier ambiguë : un ouvreur reconnu de type 1–5 sans sa condition de fin explicite échoue en échec. Un ouvreur possible de type 7 après un conteneur/contexte indenté non supporté échoue également en échec plutôt que de deviner. La même règle couvre une possible définition de référence de lien suivie de ===, où un analyseur partiel ne peut pas prouver si le setext a terminé le paragraphe. Les possibles étiquettes de lien sont suivies sur plusieurs lignes avec une gestion des backslashes dans les crochets ; un ] non échappé sans deux-points immédiat libère l'état de définition possible. La forme simple de définition plus balise sans ligne vide reste du HTML en ligne.
  • La limite de section est le titre suivant de niveau 1 ou 2. La forme populaire est ^##[^#] (exclut ###) ; la référence le durcit en ^ {0,3}#{1,2}([ \t]|$), qui traite en plus un H1 comme limite (une limite de partie ne doit jamais être engloutie dans un remplacement), accepte l'indentation de 1 à 3 espaces autorisée par CommonMark, et applique la règle d'espace de CommonMark pour qu'une ligne de paragraphe de style ##hashtag ne soit pas une fausse limite. ### n'est toujours pas une limite dans les deux cas.

Utilisez cette méthode lorsque le format du document n'est pas le vôtre à modifier, ou lorsque l'outil doit fonctionner sur du Markdown arbitraire.

Méthode 2 — marqueurs fixes

Encadrez la section maintenue par des lignes sentinelles uniques — les commentaires HTML sont invisibles dans le Markdown rendu :

<!-- managed-section:begin notes-automatisation -->
## Notes d'automatisation

...corps, modèles dans des blocs bienvenus...
<!-- managed-section:end notes-automatisation -->

Remplacez tout ce qui se trouve entre les lignes exactes de marqueur début/fin ; ajoutez tout le bloc marqué lorsque le marqueur de début est absent. Le scan des titres ne fait plus partie de la recherche de plage.

Utilisez cette méthode lorsque vous possédez le format du document : c'est la limite la plus robuste (elle survit aux renommages de titres et à tout contenu de corps), au prix de la plantation de marqueurs. Gardez les chaînes de marqueur uniques par section, et maintenez l'invariant de H2 unique ci-dessous même ici — les marqueurs protègent la plage, pas la structure de titres du document. Une note d'honnêteté : une copie littérale de la ligne de marqueur à l'intérieur d'un bloc de code confondrait toujours une recherche de marqueur aveugle aux blocs, alors soit gardez le texte du marqueur hors des exemples dans les blocs, soit trouvez les marqueurs avec le même scan conscient des blocs.

Invariants

Appliquez ces invariants avant et après chaque fusion ; ils rendent "remplacer ou ajouter" bien défini :

  1. Le bloc commence par sa propre ligne ## Titre simple — utilisez exactement un espace ASCII entre le ## d'ouverture et le contenu non vide (ou exact ## pour un H2 vide). Les espaces de séparation supplémentaires, les tabulations et une séquence de croisillon de fermeture CommonMark ne sont pas canoniques ici. Le titre fait partie du contenu fusionné, pas une configuration qui peut en dériver.
  2. Exactement un titre de niveau H1/H2 à l'intérieur du bloc — son propre premier ATX (comptage conscient des régions littérales). Un bloc avec un second ATX ou un setext H1/H2 absorbera ou divisera silencieusement une section lors d'une exécution ultérieure. Rejetez les titres setext possibles avant la première écriture afin que l'ajout et le remplacement utilisent la même limite de validation. Les littéraux de titre dans les blocs de code ou HTML bruts à l'intérieur du bloc sont acceptables — ils ne comptent pas.
  3. Au plus une copie non ambiguë du titre dans la cible (en dehors des régions littérales). Si le document contient déjà des doublons ou un alias du H2 géré (séparateur, 1 à 3 espaces, croisillon de fermeture), arrêtez-vous et signalez au lieu de "corriger" une forme et d'en laisser une autre.
  4. Une forme de séparateur canonique. Stockez le bloc sans lignes vides finales ; écrivez exactement une ligne vide entre le bloc et la section suivante. La plage de lecture et la forme d'écriture convergent alors vers les mêmes octets, ce qui rend la deuxième exécution une non-opération.
  5. Pas de blocs de code non fermés, ni dans la cible ni dans le bloc. CommonMark étend un bloc non fermé jusqu'à la fin du document, donc dans une cible, il étend silencieusement la section jusqu'à la fin du fichier (un remplacement réécrirait toute la queue visuellement engloutie), et dans un bloc, il engloutirait ce qui suit la section fusionnée. Les deux sont des entrées mal formées : arrêtez-vous et signalez.
  6. Pas de titres setext à l'intérieur du bloc canonique ou de la plage remplacée. Un soulignement === ou --- directement sous du texte de paragraphe est un vrai titre qu'un scan ^# ligne par ligne ne peut pas voir comme limite. En accepter un dans un bloc nouvellement ajouté ferait échouer la prochaine exécution au lieu de converger ; en remplacer un supprimerait une limite de section. Arrêtez-vous avant d'écrire dans les deux cas (convertissez le titre en forme ATX, ou utilisez des marqueurs fixes). Voir docs/setext-block-heading-contract.md.
  7. Pas de frontmatter d'ouverture non fermé. Lorsque la première ligne est exact --- (YAML) ou +++ (TOML), ignorez ses lignes ressemblant à des titres uniquement jusqu'au premier fermeur exact correspondant. S'il n'y a pas de fermeur, arrêtez-vous au lieu de deviner si l'ouvreur était une métadonnée ou une rupture thématique Markdown ordinaire.
  8. Pas de HTML brut de fin explicite ambigu. CommonMark laisse les types 1–5 s'étendre jusqu'à la fin du fichier lorsque leur condition de fin est absente, mais ajouter un H2 là le cacherait dans le HTML brut et briserait la convergence. Exigez la condition de fin explicite à la fois dans la cible et dans le bloc. Pour le type 7, préservez sa règle de non interruption de paragraphe et échouez en échec lorsqu'un conteneur/contexte indenté non supporté ou un contexte de référence possible plus setext, y compris une étiquette de lien multi-lignes, rend cette décision ambiguë.
  9. Espaces blancs ASCII uniquement dans le bloc. Les lignes vides, les espaces de début/fin de titre ATX et les séquences de fermeture, le contexte setext et les fermetures de blocs utilisent espace/tabulation ASCII exactement comme spécifié par CommonMark 0.31.2. Les NBSP, EM SPACE, saut de page, tabulation verticale et autres espaces Unicode restent du contenu au lieu d'être silencieusement supprimés. Voir docs/commonmark-ascii-whitespace-contract.md.

Recette de vérification

Exécutez les trois vérifications après une fusion ; ensemble, elles détectent les bugs de limite, les sections dupliquées et les modifications collatérales :

  1. Appliquer-deux-fois-diff-zéro. Appliquez la même fusion deux fois ; après la deuxième exécution, git diff doit être vide (ou comparez les octets avant/après). Pour une section située à la fin du fichier, une vérification équivalente est que le bloc est égal à la section de queue entière du fichier.

    python scripts/merge_section.py cible.md section.md
    python scripts/merge_section.py cible.md section.md   # deuxième exécution
    git diff --exit-code cible.md   # après avoir commité la première exécution : vide
    

    Le mode --check de l'implémentation de référence encode la même idée avec des codes de sortie de style diff (0 = canonique, 1 = une fusion modifierait le fichier), que CI peut appeler directement.

  2. Nombre d'occurrences du titre = 1. Comptez les lignes égales au titre avec un scan conscient des blocs. Un grep -c '^## Notes d\'automatisation$' nu est sujet au même piège que cette compétence aborde — acceptable uniquement lorsque vous savez que le corps ne contient aucune copie dans un bloc de la ligne de titre exacte.

  3. git diff --stat touche exactement un fichier — la cible. Tout autre chose signifie que l'automatisation a écrit là où elle n'aurait pas dû.

Implémentation de référence

scripts/merge_section.py implémente la Méthode 1 plus chaque invariant ci-dessus en Python 3 sans dépendances (bibliothèque standard uniquement). Python a été choisi plutôt qu'un shell pour une seule raison qui compte ici : le contrôle explicite au niveau des octets des sauts de ligne et du BOM rend l'application-deux-fois-diff-zéro prouvable, alors que les pipelines de texte shell ont tendance à normaliser les fins de ligne en coulisse. L'algorithme se porte directement dans n'importe quel langage.

python scripts/merge_section.py CIBLE.md SECTION.md            # fusion en place
python scripts/merge_section.py CIBLE.md SECTION.md --check   # vérification de dérive

SECTION.md est le bloc canonique : première ligne est exact ## Titre, avec un espace ASCII avant le contenu non vide, ou exact ## pour un H2 vide. Le style LF/CRLF de la cible et le BOM UTF-8 sont préservés ; une cible manquante est créée (ajout dans un document vide). Les octets modifiés sont vidés dans un fichier temporaire exclusif du même répertoire et installés avec un seul remplacement atomique. Le fichier temporaire commence en mode POSIX 0600 ou une DACL Windows protégée SYSTEM/Propriétaire-seulement, et son identité, ses octets, ses métadonnées et sa DACL Windows sont revérifiés avant le commit. Les attributs étendus POSIX existants (propriétaire/groupe/mode/limite) sont préservés. Pour une cible Windows existante, ReplaceFileW transmet sa DACL documentée, ses attributs de fichier et ses flux nommés ; la préservation exacte du propriétaire/groupe/SACL n'est pas garantie. Les cibles manquantes conservent les permissions privées. La cible et le bloc canonique sont lus comme des instantanés de fichiers ordinaires sans suivi de liens symboliques. Les entrées de lien symbolique, non régulières et multi-liens durs sont refusées car le remplacement changerait leur sémantique ; les points de réanalyse Windows et les fichiers Windows chiffrés par EFS sont refusés avant la lecture du contenu. L'identité, les métadonnées et les octets de la cible sont revérifiés juste avant le commit. Les modifications achevées avant cette vérification sont détectées, mais la vérification et le remplacement sont des opérations séparées ; sérialisez en externe chaque écrivain lorsqu'une cible existante ne doit pas subir de perte de mise à jour. La création de cible manquante utilise un commit sans remplacement et n'écrase jamais une création concurrente. Sous Windows, ReplaceFileW utilise un

…

[tronqué pour la traduction]

Skills associés