Communitygithub.com

h8nc4y/markdown-idempotent-section-merge

Habilidade para agentes Claude Code e Codex: substituir ou anexar de forma idempotente uma seção Markdown sem corromper blocos de código – varredura consciente de fences, marcadores fixos e receita de verificação de dupla aplicação com diff zero, com uma implementação de referência testada.

O que é markdown-idempotent-section-merge?

Esta habilidade fornece um procedimento robusto para agentes de IA (como Claude Code e Codex) manterem seções Markdown atualizadas em documentos que não são totalmente controlados por eles. Ela resolve problemas comuns como duplicação de seções, corrupção de blocos de código e não convergência após múltiplas execuções. A técnica principal é uma varredura de cabeçalhos que reconhece blocos de código, frontmatter e HTML bruto, evitando que cabeçalhos dentro de fences sejam confundidos com limites de seção. O método oferece duas abordagens: uma varredura automática de cabeçalhos e o uso de marcadores fixos (como comentários HTML) para delimitar seções. A implementação de referência em Python 3 verifica invariantes importantes, como a ausência de cabeçalhos duplicados e blocos não fechados, garantindo que a aplicação repetida da mesma seção resulte em um diff vazio (aplicação duas vezes, diff zero).

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

Perguntar na sua IA favorita

Abre um novo chat com esta habilidade de agente já pré-carregada.

Documentação

Mesclagem de Seção Markdown Idempotente

Procedimento para manter uma seção canônica ## Título em um documento Markdown sob automação: substituir a seção quando ela existe, anexá-la quando não existe e produzir saída idêntica byte a byte quando aplicada duas vezes. A parte não óbvia é onde a seção termina — e o erro clássico é confiar em uma regex ^## simples para encontrar esse limite.

Quando Usar

  • Um agente ou script mantém uma seção canônica atualizada em um documento que não é totalmente de sua propriedade — README.md, AGENTS.md, CLAUDE.md, um manual, um preâmbulo de changelog — com uma regra de "substituir se presente, anexar se ausente".
  • Você vê algum destes sintomas:
    • A seção mantida é duplicada após cada execução.
    • Uma mesclagem cortou a seção no meio de um bloco de código cercado.
    • Uma linha ## ... dentro de um bloco de código foi tratada como a próxima seção.
    • Um subtítulo ### encerrou o intervalo de substituição precocemente.
    • O atualizador nunca converge: a segunda execução ainda altera o arquivo.
    • Após uma mesclagem, tudo abaixo de algum ponto é renderizado como um bloco de código gigante (um delimitador de fence remanescente reaberto como uma nova fence).
  • Você está lendo ou contando seções com padrões do tipo ^##, não apenas escrevendo-as — extração e verificação caem na mesma armadilha.

A Armadilha: Cabeçalhos Dentro de Blocos de Código

A implementação ingênua considera o intervalo de substituição como "a linha ## X até a linha antes da próxima correspondência ^##". Ela quebra de duas maneiras.

Armadilha 1 — literais cercados. Corpos de seção legitimamente contêm linhas ## ... dentro de blocos de código cercados: um modelo de relatório, um documento de exemplo, um diff citado. Uma varredura cega a blocos de código lê erroneamente a primeira dessas linhas como o próximo limite de seção e corta o intervalo ali:

## Notas de automação

O bot atualiza esta seção.

```text
## Relatório semanal        <- varredura ^## ingênua para aqui, dentro do fence
- destaques:
- riscos:
```

Mantenha o modelo cercado para que ele não se torne um título real.

## Licença              <- a próxima seção real

Substituir até a linha cercada deixa uma cauda obsoleta para trás. A corrupção medida (veja a fixture de armadilha testada) é pior que uma simples duplicata:

  • O literal ## Relatório semanal é deixado para trás fora de qualquer fence e é renderizado como um título real, com aparência duplicada.
  • O delimitador de fechamento do bloco antigo (a linha ``` nua) também é deixado para trás, reabre como uma nova fence e engole tudo depois dela — a seção ## Licença seguinte deixa de ser um título.
  • A mesclagem não é idempotente: toda execução corresponde ao título novamente, corta em uma linha cercada novamente e aumenta o arquivo novamente.

Armadilha 2 — subtítulos ###. Uma regex ^## também corresponde a ###, #### e assim por diante. Se a seção contém subtítulos, o intervalo termina na primeira linha ### ... e as subseções antigas sobrevivem após as novas — conteúdo duplicado abaixo do ponto de mesclagem.

As mesmas duas armadilhas se aplicam a ler uma seção (extrair seu conteúdo) e a verificar uma (grep -c '^## X' pode tanto superestimar literais cercados quanto ser enganado por eles).

Limites Seguros: Dois Métodos

Método 1 — varredura de título ciente de região literal

Rastreie o estado da região literal ao escanear linhas e trate uma linha como um título ou limite apenas quando estiver fora de frontmatter, fences e blocos HTML brutos suportados:

  • Uma fence abre em uma linha cujos primeiros caracteres não espaço (no máximo 3 espaços iniciais) são uma sequência de 3+ acentos graves ou 3+ tils.
  • Ela fecha apenas em uma linha com uma sequência do mesmo caractere, pelo menos tão longa, com nada mais na linha. Uma fence não fechada vai até o final do documento (comportamento CommonMark).
  • Enquanto dentro de uma fence, nada é um título e nada é um limite.
  • Antes da varredura de fence, um bloco inicial de frontmatter de linha exata é excluído: YAML abre com --- e fecha com --- ou ... exatos; TOML abre e fecha com +++ exatos. Uma linha que parece título dentro desse bloco é conteúdo de metadados/comentário, não um limite de seção Markdown. Um abridor sem seu fechador exato falha em estado fechado antes de qualquer escrita.
  • No nível superior, coluna 0–3 blocos HTML brutos CommonMark 0.31.2 dos tipos 1–7 também são excluídos: tags de conteúdo literal (pre/script/style/textarea), comentários, instruções de processamento, declarações, CDATA, tags de bloco listadas e linhas completas de tag. Os tipos 6/7 terminam antes de uma linha em branco ou no EOF; o tipo 7 não pode interromper um parágrafo. A gramática de tag e atributo permanece apenas ASCII mesmo sob correspondência insensível a maiúsculas; equivalentes de caso Unicode são Markdown comum.
  • Estado de fence e HTML compartilham um scanner exclusivo, então delimitadores dentro da outra região permanecem dados literais.
  • Esta referência mutante é intencionalmente mais restrita que o CommonMark em um EOF ambíguo: um abridor reconhecido do tipo 1–5 sem sua condição de fim explícita falha fechado. Um possível abridor do tipo 7 após um contexto de contêiner/recuo não suportado também falha fechado em vez de adivinhar. A mesma regra cobre uma possível definição de referência de link seguida por ===, onde um analisador parcial não pode provar se o setext encerrou o parágrafo. Possíveis rótulos de link são rastreados entre linhas com manipulação de colchetes cientes de escape; um ] sem escape sem dois pontos imediatos libera o estado de possível definição. A forma simples de definição-mais-tag sem linha em branco permanece HTML inline.
  • O limite da seção é o próximo título de nível 1 ou 2. A forma popular é ^##[^#] (exclui ###); a referência o endurece para ^ {0,3}#{1,2}([ \t]|$), que adicionalmente trata um H1 como um limite (um limite de parte nunca deve ser engolido em uma substituição), aceita o recuo de 1–3 espaços que o CommonMark permite e aplica a regra de espaço do CommonMark para que uma linha de parágrafo do tipo ##hashtag não seja um limite falso. ### ainda não é um limite de qualquer maneira.

Use este método quando o formato do documento não é seu para alterar, ou quando a ferramenta deve funcionar em Markdown arbitrário.

Método 2 — marcadores fixos

Envolva a seção mantida em linhas de sentinela exclusivas — comentários HTML são invisíveis no Markdown renderizado:

<!-- managed-section:begin notas-automacao -->
## Notas de automação

...corpo, modelos cercados bem-vindos...
<!-- managed-section:end notas-automacao -->

Substitua tudo entre as linhas exatas de marcador de início/fim; anexe todo o bloco marcado quando o marcador de início estiver ausente. A varredura de título não faz mais parte da localização do intervalo.

Use este método quando você possui o formato do documento: é o limite mais robusto (sobrevive a renomeações de título e qualquer conteúdo do corpo), ao custo de plantar marcadores. Mantenha as strings de marcador exclusivas por seção e mantenha a invariante de H2 único abaixo mesmo aqui — os marcadores protegem o intervalo, não a estrutura de títulos do documento. Uma nota de honestidade: uma cópia literal da linha de marcador dentro de um bloco de código ainda confundiria uma busca de marcador cega a blocos, então mantenha o texto do marcador fora de exemplos cercados ou encontre marcadores com a mesma varredura ciente de blocos.

Invariantes

Imponha estas antes e depois de cada mesclagem; elas tornam "substituir ou anexar" bem definido:

  1. O bloco começa com sua própria linha ## Título simples — use exatamente um espaço ASCII entre o ## de abertura e o conteúdo não vazio (ou ## exato para um H2 vazio). Espaços/tabs separadores extras e uma sequência de cerquilha de fechamento CommonMark não são canônicos aqui. O título faz parte do conteúdo mesclado, não configuração que pode se desviar dele.
  2. Exatamente um título de nível H1/H2 dentro do bloco — sua própria primeira linha ATX (contagem ciente de região literal). Um bloco com um segundo H1/H2 ATX ou setext absorveria ou dividiria silenciosamente uma seção em uma execução posterior. Rejeite possíveis títulos setext antes da primeira escrita para que anexo e substituição usem o mesmo limite de validação. Literais de título cercados ou em HTML bruto dentro do bloco são aceitáveis — eles não contam.
  3. No máximo uma cópia inequívoca do título no destino (fora de regiões literais). Se o documento já contém duplicatas ou um alias separador / 1–3 espaços / cerquilha de fechamento do H2 gerenciado, pare e relate em vez de "corrigir" uma forma e deixar outra.
  4. Uma forma de separador canônica. Armazene o bloco sem linhas em branco finais; escreva exatamente uma linha em branco entre o bloco e uma seção seguinte. O intervalo de leitura e a forma de escrita então convergem para os mesmos bytes, que é o que torna a segunda execução um no-op.
  5. Nenhuma fence não fechada, no destino ou no bloco. CommonMark leva uma fence não fechada até o final do documento, então em um destino ela estende silenciosamente a seção até o EOF (uma substituição reescreveria toda a cauda visualmente engolida), e em um bloco ela engoliria o que vier após a seção mesclada. Ambos são entrada malformada: pare e relate.
  6. Nenhum título setext dentro do bloco canônico ou intervalo substituído. Um sublinhado === ou --- diretamente sob texto de parágrafo é um título real que uma varredura linha a linha ^# não pode ver como limite. Aceitar um em um bloco recém-anexado faria a próxima execução falhar em vez de convergir; substituir ao longo de um excluiria um limite de seção. Pare antes de escrever em ambos os casos (converta o título para a forma ATX ou use marcadores fixos). Veja docs/contrato-titulo-bloco-setext.md.
  7. Nenhum frontmatter inicial não fechado. Quando a primeira linha é --- (YAML) ou +++ (TOML) exata, ignore suas linhas que parecem título apenas através do primeiro fechador exato correspondente. Se nenhum fechador existir, pare em vez de adivinhar se o abridor era metadados ou uma quebra temática Markdown comum.
  8. Nenhum HTML bruto de fim explícito ambíguo. CommonMark permite que tipos 1–5 vão até o EOF quando sua condição de fim está faltando, mas anexar um H2 lá o esconderia dentro de HTML bruto e quebraria a convergência. Exija a condição de fim explícita tanto no destino quanto no bloco. Para o tipo 7, preserve sua regra de não interrupção de parágrafo e falhe fechado quando um contexto de contêiner/recuo não suportado ou possível referência-mais-setext, incluindo um rótulo de link de várias linhas, tornar essa decisão ambígua.
  9. Espaço em branco de bloco apenas ASCII. Linhas em branco, aparagem de título ATX e sequências de fechamento, contexto setext e fechadores de fence usam espaço/tab ASCII exatamente como CommonMark 0.31.2 especifica. NBSP, EM SPACE, alimentação de formulário, tabulação vertical e outros espaços em branco Unicode permanecem como conteúdo em vez de serem removidos silenciosamente. Veja docs/contrato-espaco-branco-ascii-commonmark.md.

Receita de Verificação

Execute todas as três verificações após uma mesclagem; juntas elas capturam bugs de limite, seções duplicadas e edições colaterais:

  1. aplicar duas vezes diff zero. Aplique a mesma mesclagem duas vezes; após a segunda execução git diff deve estar vazio (ou compare bytes antes/depois). Para uma seção que fica no final do arquivo, uma verificação equivalente é que o bloco é igual a toda a seção final do arquivo.

    python scripts/mesclar_secao.py destino.md secao.md
    python scripts/mesclar_secao.py destino.md secao.md   # segunda execução
    git diff --exit-code destino.md   # após commitar a primeira execução: vazio
    

    O modo --check da implementação de referência codifica a mesma ideia com códigos de saída no estilo diff (0 = canônico, 1 = uma mesclagem alteraria o arquivo), que CI pode chamar diretamente.

  2. Contagem de ocorrências do título = 1. Conte linhas iguais ao título com uma varredura ciente de blocos. Um simples grep -c '^## Notas de automação$' está sujeito à mesma armadilha sobre a qual esta habilidade trata — aceitável apenas quando você sabe que o corpo não contém uma cópia cercada da linha exata do título.

  3. git diff --stat toca exatamente um arquivo — o destino. Qualquer outra coisa significa que a automação escreveu onde não deveria.

Implementação de Referência

scripts/mesclar_secao.py implementa o Método 1 mais todas as invariantes acima em Python 3 sem dependências (apenas biblioteca padrão). Python foi escolhido em vez de shell por uma razão que importa aqui: controle explícito em nível de byte de novas linhas e BOM torna a aplicação-duas-vezes-diff-zero demonstrável, enquanto pipelines de texto shell tendem a normalizar finais de linha nas suas costas. O algoritmo é portável diretamente para qualquer linguagem.

python scripts/mesclar_secao.py DESTINO.md SECAO.md            # mesclar no lugar
python scripts/mesclar_secao.py DESTINO.md SECAO.md --check   # verificação de deriva

SECAO.md é o bloco canônico: a primeira linha é ## Título exata, com um espaço ASCII antes do conteúdo não vazio, ou ## exato para um H2 vazio. O estilo LF/CRLF e BOM UTF-8 do destino são preservados; um destino ausente é criado (anexo em um documento vazio). Bytes alterados são liberados para um arquivo temporário exclusivo no mesmo diretório e instalados com uma substituição atômica. O temporário começa com modo POSIX 0600 ou um DACL Windows protegido SYSTEM/Proprietário-Apenas, e sua identidade, bytes, metadados e DACL Windows são reverificados antes do commit. Atributos estendidos POSIX existentes de proprietário/grupo/modo/limitados são preservados. Para um destino Windows existente, ReplaceFileW transporta seu DACL documentado, atributos de arquivo e fluxos nomeados; preservação exata de proprietário/grupo/SACL não é prometida. Destinos ausentes retêm as permissões privadas.

Tanto o destino quanto o bloco canônico são lidos como snapshots de arquivo comum sem seguir links. Entradas de link simbólico, não regulares e multi-hard-link são recusadas porque a substituição mudaria sua semântica; pontos de reanálise Windows e arquivos Windows criptografados por EFS são recusados antes que o conteúdo seja lido. A identidade, metadados e bytes do destino são reverificados imediatamente antes do commit. Alterações concluídas antes dessa verificação são detectadas, mas a verificação e a substituição são operações separadas; serializa externamente cada escritor quando um destino existente não deve sofrer uma atualização perdida. A criação de destino ausente usa um commit sem substituição e nunca sobrescreve uma criação concorrente. No Windows, ReplaceFileW usa um ...

...[truncado para tradução]

Habilidades Relacionadas