Pesquisar

iainfrastructure

Atualização do meu script AI-Powered Markdown Translator (v1.9): novidades e como buscar código limpo sem revisar em pair-IA

Atualização do meu script AI-Powered Markdown Translator (v1.9): novidades e como buscar código limpo sem revisar em pair-IA

ai-powered-markdown-translator

Artigo traduzido do fr para o pt com gpt-5.4-mini.

Ver projeto no GitHub ↗

AI-Powered Markdown Translator é um projeto open-source que mantenho desde 2024: um script Python que traduz qualquer arquivo Markdown para 14 idiomas via 4 provedores de IA (OpenAI, Mistral AI, Claude, Gemini). Ele alimenta este blog a cada publicação — toda página que você lê aqui em um idioma diferente do francês passou por ele — e quase 1 800 versões traduzidas rodam graças a ele em produção.

Em 8 de maio de 2026, publiquei a v1.9, que reúne 75 commits e marca a maior atualização desde a v1.5 de 2024. Três novidades de produto:

  1. Validação pós-tradução (anti-falha silenciosa)
  2. Nota de tradução multi-posicionamento (no topo, no rodapé ou em ambos)
  3. Modo --news para preservar as citações-fonte em EN

Mas essa v1.9 tem uma particularidade que quero contar aqui: todo o código foi escrito em pair-IA. Nem uma linha digitada à mão. Então, além das 3 novidades, o artigo aborda o “como”: quais guardrails colocar em prática para buscar código limpo e seguro quando você não revisa você mesmo o que a IA produz?

O contexto: um projeto usado todos os dias, pouco mantido do lado do código

De setembro de 2024 a maio de 2026: uso contínuo, manutenção em ondas

Eu havia publicado um artigo que detalhava o código-fonte da v1.5 em 2024. Na época, eu publicava o script diretamente no artigo. Hoje, o ângulo mudou: o que importa não é mais tanto o código que eu escrevo, e sim o fluxo de trabalho que o produz.

Entre a v1.5 publicada em setembro de 2024 e janeiro de 2026, o projeto continuou rodando — ele traduz cada novo conteúdo deste blog — mas o código público quase não se mexeu. Um único commit foi enviado em 2025. Durante todo esse tempo, eu fui evoluindo o código localmente para minhas necessidades pessoais — principalmente os modelos, que eu substituía conforme iam sendo lançados — mas essas evoluções ficavam na minha máquina. A versão pública no GitLab continuava apontando para os valores padrão da v1.5.

No início de 2026, fiz um primeiro esforço de atualização: três releases em dois meses (v1.6 e v1.7 em dois dias no começo de janeiro, v1.8 em março) que atualizaram o projeto do lado funcional — modelos de 2026, suporte a Gemini, modo --eco, arquivo único, modo --news para as citações-fonte. Mas ainda sem CI, sem testes automatizados, sem gates de qualidade — o que me colocava um problema real para ir mais longe com um agente de IA que codifica no meu lugar.

O ritmo de um projeto no tempo livre

Por que esse descompasso? Porque eu conduzo esse projeto no meu tempo livre. Tenho família, uma vida fora da tela, então a evolução só avança em ondas quando encontro as noites e os fins de semana. Sou apaixonado pelo tema, passo mesmo bastante tempo nesses assuntos — testo muito, guio os agentes, valido os resultados — mas o ritmo não é o de um projeto profissional.

O pair-IA muda justamente isso. Ele me permite avançar entre duas limitações — a paixão e o equilíbrio da vida fora da tela. Sem pair-IA, eu claramente não iria tão longe nem tão rápido. Com ele, consigo manter um projeto open-source em nível industrial sem dedicar minha vida a isso.

O objetivo inicial: qualidade + migração GitLab → GitHub

Em meados de abril de 2026, quis finalmente cuidar disso de forma séria. Dois objetivos simples:

  1. Adicionar uma camada de qualidade (análise estática, testes, CI)
  2. Migrar o repo do GitLab para o GitHub

Nada além disso. Só que, com um agente de código em pair-IA, a gente nunca escreve exatamente o que tinha previsto. A PR acabou em 75 commits, 9 837 additions, 1 982 suppressions, 58 arquivos.

VersãoDataPrincipal contribuição
1.0–1.42024OpenAI, depois Mistral, depois Claude
1.5set. 2024Refatoração dos clientes, modelos 2024 (gpt-4o, claude-3.5-sonnet)
1.6jan. 2026Modelos 2026 (gpt-5, claude-sonnet-4-5, gemini-3-pro), Gemini, modo --eco, arquivo único (--file)
1.7jan. 2026--keep_filename, .env, código inline preservado
1.8mar. 2026Modelos GPT-5.4 por padrão, modo --news com placeholders de citações
1.9mai. 2026Validação pós-tradução, nota multi-posicionamento, stack de qualidade 14 hooks + 229 testes + revisão IA

O efeito bola de neve

Cada ferramenta de qualidade adicionada revelava problemas. O Codacy apontou duplicações. O SonarCloud levantou code smells (sinais de que o código vai envelhecer mal: funções longas demais, parâmetros não usados, estruturas complicadas). /pr-review-toolkit apontou bugs ocultos. A cada sinal, o agente corrigia, às vezes melhorando também coisas adjacentes.

O escopo cresceu organicamente. Era exatamente o que eu queria — modernizar o projeto —, mas a dosagem do esforço era ditada pelas ferramentas, não por mim. Para um projeto em vibe coding, esse é um ponto-chave: as ferramentas de qualidade orientam o trabalho tanto quanto o verificam.

Novidade 1: a validação pós-tradução (anti-falha silenciosa)

O incidente: foi a IA que encontrou o bug, durante os testes

Ao testar a PR em READMEs de diferentes repositórios públicos — um caso que nenhuma fixture cobria —, a IA apontou o que eu tinha perdido: em certos idiomas (notadamente o hindi, código ISO hi), trechos continuavam no idioma-fonte no meio da tradução. A API tinha retornado 200, o script tinha gravado o arquivo, mas o conteúdo estava parcialmente traduzido. E isso passava pela bateria de testes unitários existente — que não cobria esse caso real multilíngue.

Esse é exatamente o tipo de bug que o vibe coding pode produzir e que ninguém vê. O código parece lógico, as fixtures de teste não cobrem o caso, o humano não revisa o resultado. Só que, ao testar o script em casos reais (multi-repo), a própria IA fez o que as fixtures não faziam.

O que levo disso: testes práticos multi-repo encontram o que os testes unitários não pegam. E a IA também pode servir para descobrir bugs de agentes de IA anteriores — desde que você a coloque diante de casos reais e variados.

Foi nesse momento que percebi que era preciso adicionar uma validação pós-tradução de verdade. É essa primeira novidade que detalho agora: a camada dupla de validação.

A camada dupla de validação

EtapaAçãoSe KO
1️⃣Chamada de API do provedorExceção de rede → ❌ failure
2️⃣Lista de permissões por provedor para finish_reason (ou stop_reason no Claude)Fora da whitelist → ❌ failure
3️⃣Anti-vazamento: nenhuma janela-fonte ≥ 120 chars verbatim na saídaJanela-fonte encontrada → ❌ failure
4️⃣langdetect.detect_langs (probabilidades fonte vs target)Fonte > 0,80 E target < 0,20 → ❌ failure
5️⃣Empty-content + ratio saída/fonte (se fonte ≥ 500 chars)Vazio ou saída < max(50, source/20) → ❌ failure
SUCESSOexit code 0

Camada 1 (determinística) — Primeiro filtro: verificar o status devolvido pela API. Cada provedor expõe um campo finish_reason (ou stop_reason no Claude) que indica por que o LLM parou de gerar. O script mantém uma whitelist por provedor dos statuses aceitáveis — a nomenclatura varia (stop no OpenAI/Mistral, STOP ou FINISH_REASON_STOP no Gemini, end_turn ou stop_sequence no Claude). O código também tolera None por segurança, quando o SDK não devolve esse campo. Qualquer outro status — por exemplo length, max_tokens ou MAX_TOKENS conforme o provedor, que sinalizam uma resposta interrompida pelo limite de tokens — dispara um RuntimeError imediato, sem tentativa de recuperação.

Segundo filtro determinístico, mais sutil: verificar que nenhum trecho do texto-fonte apareça verbatim na saída traduzida. Na prática, extraímos janelas de 120 caracteres ou mais do texto-fonte; se uma delas for encontrada exatamente assim na saída, é porque não foi traduzida — failure. Foi exatamente esse check que resgatou o caso do hindi: o LLM tinha respondido stop (ou seja, fim “natural” do lado da API), mas parágrafos em francês tinham permanecido intactos na saída — invisíveis ao filtro finish_reason, detectados pelo filtro anti-vazamento verbatim.

Camada 2 (probabilística)langdetect.detect_langs analisa o idioma da saída e retorna uma distribuição de probabilidades sobre várias línguas candidatas. Extraímos a probabilidade do idioma-fonte e a do idioma-alvo, e então rejeitamos somente se a prob fonte ultrapassar 0,80 e a prob target cair abaixo de 0,20 — um limiar deliberadamente conservador para não gerar falsos positivos em code-switching técnico (palavras em inglês legítimas numa tradução em francês, por exemplo). Essa camada faz short-circuit para idiomas com scripts não latinos (hindi hi, árabe ar, chinês zh, japonês ja, coreano ko), onde um sinal de script suficiente já valida a saída. E ela só roda se a saída limpa tiver pelo menos 100 caracteres, para evitar falsos positivos em texto curto demais.

Os guardrails quantitativos

Acima das duas camadas, dois controles mais prosaicos, mas necessários:

  • Proteção contra conteúdo vazio: se o provedor devolve uma saída vazia enquanto finish_reason é stop, rejeitamos imediatamente (caso contrário, escreveríamos um arquivo vazio marcado como sucesso)
  • Relação de sanidade: somente se a fonte tiver pelo menos 500 caracteres, verificamos que a saída não ficou suspeitamente curta (tipicamente < max(50, source/20)). Isso é um detector de truncamento invisível, não uma regra geral de comprimento

No Claude especificamente, max_tokens passou de 4 096 para 32 768 na v1.9 (a alteração foi feita no código pelo Claude depois que eu constatei o sintoma e pedi para investigar). A razão documentada no CHANGELOG: evitar truncamento latente em segmentos de 16 k caracteres, com margem adicional para idiomas em script não latino (FR → JA, ZH, KO, AR, HI) que consomem mais tokens na saída do que um script latino equivalente.

Retornos por status explícito

O pipeline de arquivo (translate_markdown_file()) agora devolve um status explícito — success, failure ou skipped. O CLI agrega esses statuses e termina com um código de saída não nulo assim que pelo menos um arquivo falha — o que torna a falha aproveitável por um script chamador ou pela nova CI adicionada na v1.9. Antes desta v1.9, vários erros eram apenas impressos ou passavam como uma tradução bem-sucedida: o processo podia terminar em 0 enquanto o arquivo estava ausente, incompleto ou mal validado. O status skipped passa a ser, ele próprio, um sinal legível (“ignorado de propósito”), distinto de success (“tradução gravada corretamente”).

📄 Trecho Python: validação dupla pós-tradução (translate.py)
def _check_passthrough_excerpt(segment, stripped, args):
    """Couche 1 : vérifie qu'aucune fenêtre source ≥120 chars (cleaned) n'apparaît
    verbatim dans la sortie (bug silent-failure typique : LLM renvoie le source brut)."""
    out_norm = re.sub(r"\s+", " ", stripped).casefold()
    for window in _extract_source_windows(segment, ignore_blockquotes=args.news):
        if _looks_like_proper_noun_list(window):
            continue
        window_norm = re.sub(r"\s+", " ", window).casefold()
        if window_norm in out_norm:
            raise RuntimeError(
                f"Output contains untranslated source excerpt "
                f"(model={args.model}, target={args.target_lang}, "
                f"matched window: {window_norm[:100]!r})"
            )


def _check_output_language(stripped, args):
    """Couche 2 : langdetect probabiliste sur la langue de sortie. Court-circuite
    si target script (HI/AR/ZH/JA/KO) déjà détecté en quantité suffisante (le
    code-switching technique fait que langdetect peut sous-estimer la cible).
    """
    if _has_target_script_signal(stripped, args.target_lang):
        return
    langdetect_text = _clean_for_language_detection(stripped)
    if len(langdetect_text) < 100:
        return
    probas = {p.lang: p.prob for p in detect_langs(langdetect_text)}
    # ... seuils source/target appliqués pour décider si on rejette

Novidade 2: a nota de tradução multi-posicionamento

A necessidade: uma nota discreta, mas informativa

Quando o AI-Powered Markdown Translator escreve uma tradução, ele adiciona uma nota de tradução que indica o modelo usado e a data. Antes da v1.9, essa nota era sempre colada no fim do arquivo, em um formato legado (legacy) com delimitadores visíveis.

O formato colado no fim trazia dois problemas para os meus próprios usos. Primeiro, o leitor só era informado no final de que o conteúdo tinha sido traduzido por IA — é melhor avisar logo no começo, isso cria uma expectativa correta sobre o conteúdo. Depois, a nota no rodapé não destacava o projeto de tradução que torna tudo isso possível: você lê o artigo, e a origem do fluxo multilíngue passa despercebida. Eu queria, portanto, poder mover a nota para o topo sem perder a rastreabilidade — sem quebrar os usos existentes. A v1.9 adiciona dois flags que não quebram nada:

  • --note_position {top,bottom,both} : topo, rodapé ou ambos
  • --note_format {legacy,marker} : formato legado ou formato marcador (marker format)

Padrões retrocompatíveis: legacy + bottom. Nenhuma cadeia de tradução existente muda de comportamento por padrão — ativamos explicitamente os novos flags sob demanda.

O formato marcador: um cartão incorporado GitHub limpo

O formato marcador explora um detalhe sutil do Markdown do GitHub: as link reference definitions não usadas ficam invisíveis na renderização. Podemos, então, codificar metadados (modelo, data, origem) em um comentário-marker posicionado no topo do arquivo — invisível no navegador, mas preservado como está na cópia bruta.

Além disso, o GitHub gera um cartão incorporado (embed card) quando compartilhamos um link para o arquivo traduzido, e esse cartão exibe corretamente o título do documento sem poluição textual.

Exemplo de Markdown bruto com o formato marcador na posição top :

[//]: # 'translation-marker: model=claude-sonnet-4-5 date=2026-05-08 source=fr target=en'

# Title of the article in target language

Body of the translated content...

A olho nu, o leitor vê apenas o título seguido do conteúdo. O marcador não polui nem a renderização HTML nem o cartão embed.

A inserção consciente do frontmatter (frontmatter-aware)

Detalhe técnico, mas crucial: inserir uma nota em top não significa « inserir na linha 1 do arquivo ». Se o arquivo tem um frontmatter YAML (o que é o caso deste blog), é preciso inserir depois do frontmatter — caso contrário, a nota quebra o YAML.

Eu passei o requisito ao Claude (« insere a nota depois do frontmatter, não antes — caso contrário você quebra o YAML »), e ele produziu um helper _split_frontmatter que detecta as fences --- de abertura/fechamento. Se o arquivo tem uma fence YAML não fechada (caso malformado), o helper lança uma RuntimeError em vez de produzir silenciosamente um arquivo quebrado. A passagem de uma função monolítica para 7 helpers puros (separados e testáveis) é típica do que um pair-IA bem orientado consegue fazer rápido. Meu papel aqui: guia do requisito, testador, cliente final que valida o resultado. Não programar. Neste projeto, eu visto vários chapéus — exceto o de escrever o código, que cabe ao Claude.

PosiçãoFormatoCaso de uso típico
topmarkerArtigos de blog (nota discreta, card embed limpo)
toplegacyDoc interna em que a rastreabilidade visível importa
bottommarkerREADME open-source (coerente com o footer)
bottomlegacyPadrões — compatível com versões anteriores
bothmarkerArtigos longos em que topo + rodapé tranquilizam
bothlegacyCasos legacy com exigência de dupla rastreabilidade
📄 Trecho Python : helper _split_frontmatter (translate.py)
def _split_frontmatter(content):
    lines = content.splitlines(keepends=True)
    if not lines or lines[0].strip() != "---":
        return "", content
    for index in range(1, len(lines)):
        if lines[index].strip() == "---":
            frontmatter = "".join(lines[: index + 1]).rstrip("\n")
            body = "".join(lines[index + 1 :]).lstrip("\n")
            return frontmatter, body
    # Opening `---` sans fence de fermeture : insérer la note sans erreur
    # produirait un fichier mal formé. On préfère faire échouer le fichier
    # (failed_files dans translate_markdown_file) plutôt qu'écrire un output cassé.
    raise RuntimeError("malformed frontmatter: opening '---' without closing fence")

Novidade 3 : o modo --news para preservar as citações-fonte EN

O problema : traduzir sem quebrar as citações

Quando escrevo artigos ia-actualites para este blog (notícias diárias/semanais de IA multi-source), cito regularmente tweets, posts de blog, anúncios de versão em inglês — muitas vezes vários por artigo. Se a tradução mexe nas citações, elas viram falsas.

Uma citação traduzida é uma citação alterada. Em todas as versões linguísticas (EN, DE, JA etc.), queremos manter o inglês original das citações — é uma exigência de fidelidade às fontes — acompanhado da bandeira do idioma-alvo e de uma tradução em itálico para o conforto de leitura.

A solução : placeholders <NEWSQUOTE id="N"/>

EtapaAção
1️⃣Markdown fonte FR com citações EN na entrada
2️⃣Pré-processamento : extração das citações EN, substituição por placeholders <NEWSQUOTE id="0"/>, <NEWSQUOTE id="1"/>, etc.
3️⃣Tradução via API (FR → target_lang) — as citações EN originais nunca são enviadas ao LLM, apenas os placeholders são (preservados exatamente como estão)
4️⃣Pós-processamento : restauração dos placeholders com as citações EN originais intactas + inserção da bandeira do idioma-alvo
5️⃣Validação pós-tradução : todos os placeholders foram restaurados ?
Saída-alvo com citações EN preservadas
Falha se o placeholder não for restaurado ou se a citação tiver sido alterada

O modo --news se baseia nesse princípio : um pré-processamento extrai todas as citações EN, substitui por placeholders do tipo <NEWSQUOTE id="0"/>, traduz o resto e restaura os placeholders intactos.

O mapeamento LANG_FLAGS adapta a bandeira ao target_lang (15 idiomas cobertos) : 🇬🇧 para o inglês, 🇩🇪 para o alemão, 🇪🇸 para o espanhol, 🇮🇹 para o italiano, 🇵🇹 para o português, 🇳🇱 para o neerlandês, 🇵🇱 para o polonês, 🇸🇪 para o sueco, 🇷🇴 para o romeno, 🇸🇦 para o árabe, 🇮🇳 para o hindi, 🇯🇵 para o japonês, 🇰🇷 para o coreano, 🇨🇳 para o chinês, 🇫🇷 para o francês.

A validação pós-tradução verifica que todos os placeholders foram restaurados intactos. O erro não é um « vazamento EN » — o EN é intencional — mas um placeholder não restaurado ou uma citação alterada.

Casos de uso atuais e perspectivas

Hoje, uso --news exclusivamente nos artigos ia-actualites do blog. No futuro, isso poderia se estender a qualquer artigo que misture prosa em francês e citações-fonte em EN — entrevistas, relatos de experiência que citam artigos de pesquisa em inglês, transcrições de apresentações de conferência.

Sem reler o código : por que é preciso dobrar as proteções

« Eu não leio o código. »

Eu não releio nada. Às vezes dou uma olhada rápida num diff — é raro, e só quando o Claude não consegue se virar sozinho em algum ponto. Aqui está o fluxo que uso no dia a dia e que produziu a v1.9 : Claude Code (Opus, exclusivamente) escreve o código. Codex assume quando o Opus trava ou quando a janela de uso fica saturada. GPT-5.5 em reasoning extra-high desafia os planos antes da execução. /pr-review-toolkit:review-pr revisa a PR antes de cada merge. Meu papel termina em validar as direções e definir as proteções.

Esse modo de desenvolvimento — desenvolvimento integral ao feeling (vibe coding) — não é falta de rigor. É um compromisso explícito : menos revisão humana, mais validação instrumentada. As 3 novidades v1.9 que acabei de apresentar foram todas produzidas nesse fluxo. E é precisamente porque não se relê o código que é preciso dobrar as proteções técnicas — não suprimi-las.

Aqui estão as duas proteções implementadas para tornar esse modo de desenvolvimento viável em produção : uma stack de qualidade automatizada (Proteção 1) e uma revisão assistida por IA em fluxo multimodelo (Proteção 2).

Proteção 1 : a stack de qualidade automatizada (14 hooks + testes práticos)

Visão geral

RedeFerramentasCusto típicoBloqueia se falhar
pre-commitshellcheck, ruff, prettier, pre-commit-hooks (8 sub-hooks), detect-secrets, Lizard CCN< 10 sSim
pre-pushmypy, Opengrep SAST, pip-audit + audit_verdict, unittest (229)~ 30 sSim, exceto pip-audit em reporting initial
CI externaSonarCloud, Codacy, CodeFactorem paraleloNão bloqueia localmente, badges PR

Números v1.9 : 14 hooks, 229 testes unittest stdlib, ~98 % de cobertura no novo código v1.9, 11 badges SonarCloud, 3 plataformas externas.

Pre-commit : a rede rápida

#FerramentaVersãoFunção
1shellcheck-py0.10.0.1Lint shell
2ruff (lint)0.8.6Lint Python
3ruff (format)0.8.6Formatação Python
4prettier3.1.0Formatação Markdown / JSON / YAML
5trailing-whitespace5.0.0Remoção de espaços em branco no fim da linha
6end-of-file-fixer5.0.0Newline final obrigatória
7check-yaml5.0.0Validação de sintaxe YAML
8check-toml5.0.0Validação de sintaxe TOML
9check-added-large-files5.0.0Bloqueia binários grandes adicionados por acidente
10check-merge-conflict5.0.0Detecção dos marcadores de conflito Git
11check-executables-have-shebangs5.0.0Verifica se os executáveis têm um shebang
12check-shebang-scripts-are-executable5.0.0Verifica se os scripts com shebang são executáveis
13detect-secrets1.5.0Detecção de chaves de API e segredos
14check-complexity (Lizard)localTeto de complexidade ciclomática no novo código

Total medido : cerca de 2 a 3 segundos em todo o repositório (a quente, pre-commit run --all-files cronometrado em ~2,4 s). Em um commit médio que toca só alguns arquivos, é ainda mais rápido. A regra prática que aplico : acima de 10 s, os desenvolvedores contornam (o pair-IA também) — então é preciso manter essa rede rápida o tempo todo.

Pre-push : a rede pesada

  • mypy em modo relaxado : sem strict total (o código histórico de translate.py não passaria), mas com uma verificação de progressão no novo código
  • Opengrep SAST : p/security-audit p/default p/python — cerca de 30 segundos para escanear injeções, eval, deserialização insegura
  • pip-audit encapsulado por scripts/check-pip-audit.sh : captura a saída JSON, classifica no shell os erros de transporte (rede, PyPI fora do ar) para não confundir vulnerabilidade com indisponibilidade, e reporta as vulnerabilidades. Em modo reporting initial para a v1.9 (warn + exit 0) — a endurecer em modo bloqueante depois de uma PR de bump das dependências obsoletas.
  • unittest discovery : python -m unittest discover em tests/ depois scripts/tests/ — 229 testes, cerca de 8 segundos localmente

CI externa : SonarCloud + Codacy + CodeFactor

O workflow .github/workflows/sonarcloud.yml (project key jls42_ai-powered-markdown-translator) roda em cada PR. 11 badges SonarCloud exibidos no README : Quality Gate, classificação de Segurança/Confiabilidade/Manutenibilidade, Cobertura, Vulnerabilidades, Bugs, Code Smells, Duplicações, Dívida técnica, LOC.

Por que a redundância Codacy + SonarCloud + CodeFactor ? Porque cada um vê coisas diferentes. O Codacy sinalizou duplicações que o SonarCloud não tinha apontado. O SonarCloud sinalizou indícios de baixa qualidade (os famosos code smells) que o Codacy deixava passar. O CodeFactor sinalizou problemas de complexidade que os dois outros ignoraram. Nenhum deles bastaria sozinho. O custo marginal de uma plataforma adicional é nulo (badge gratuito, integração de 5 minutos), então multiplicamos os pontos de vista.

Testes : unittest stdlib (não pytest)

229 testes, 0 regressão nos 6 meses da PR, ~98 % de cobertura no novo código v1.9.

Detalhe típico :

  • test_silent_failure.py : 97 testes focados na dupla validação
  • test_orchestration.py : 79 testes no pipeline orquestrador
  • test_translation_note_position.py : 38 testes na matriz posição × formato
  • test_audit_verdict.py : 15 testes no wrapper pip-audit (em scripts/tests/)

Nota de honestidade : a cobertura de ~98 % diz respeito ao novo código v1.9 — não ao conjunto histórico de translate.py, que ainda contém algumas funções herdadas pouco cobertas pela nova bateria de testes. Eu menciono isso explicitamente porque anunciar « 98 % de cobertura » em um projeto inteiro seria enganoso.

Escolha discutível, mas assumida : executor de testes unittest (stdlib), não pytest. O prefixo test_ é por hábito, mas é unittest que executa. Por quê ? Em um projeto em vibe coding, cada dependência adicionada = cada dependência que a IA pode usar mal. Simplicidade é um objetivo. unittest faz parte da biblioteca padrão do Python, zero instalação, zero plugin.

Testes práticos : multi-repo + uso interno do produto (dogfooding) + verificação da renderização visual

Os 229 testes unittest não bastam. Eu adiciono três camadas de teste prático :

1. Multi-repo — testar o script em vários repositórios públicos com READMEs em formatos diferentes. Isso revela casos-limite que os fixtures não cobrem — um README com 8 níveis de heading, outro com shortcodes herdados, um terceiro com código embutido exótico. Foi nessa fase que o incidente de silent-failure da Novidade 1 foi descoberto.

2. Dogfooding no blog — jls42.org é traduzido pelo próprio script. Cada artigo publicado é um teste ao vivo em produção. Se um caso-limite passar pelos testes unitários, ele aparecerá aqui, na página que você está lendo. Esse é o teste supremo — o que está on-line é o que o projeto produziu.

3. Teste de renderização visual — verifico se as traduções renderizadas aparecem corretamente, seja no navegador (página web final), seja diretamente no VSCode via um plugin de pré-visualização Markdown. A ideia : não se contentar com um Markdown sintaticamente válido, mas ver a renderização real. As renderizações visuais revelam bugs de aparência (tabelas quebradas, code blocks malformados, frontmatter interpretado errado) que os testes de texto não enxergam.

As IAs também participam desses testes. /pr-review-toolkit executa o código em ambiente de teste, e o uso em pair-IA inclui sistematicamente passagens de validação visual (« verifique se a tradução alemã da página X aparece corretamente »).

📄 Trecho .pre-commit-config.yaml (hooks principais de pre-commit)
repos:
  - repo: https://github.com/shellcheck-py/shellcheck-py
    rev: v0.10.0.1
    hooks:
      - id: shellcheck
        args: ['-x']

  - repo: https://github.com/astral-sh/ruff-pre-commit
    rev: v0.8.6
    hooks:
      - id: ruff
        args: [--fix, --exit-non-zero-on-fix]
      - id: ruff-format

  - repo: https://github.com/pre-commit/mirrors-prettier
    rev: v3.1.0
    hooks:
      - id: prettier
        files: \.(json|yaml|yml|md)$

  - repo: https://github.com/pre-commit/pre-commit-hooks
    rev: v5.0.0
    hooks:
      - id: trailing-whitespace
      - id: end-of-file-fixer
      - id: check-yaml
      - id: check-toml
      - id: check-added-large-files
        args: [--maxkb=1000]
      - id: check-merge-conflict
      - id: check-executables-have-shebangs
      - id: check-shebang-scripts-are-executable

  - repo: https://github.com/Yelp/detect-secrets
    rev: v1.5.0
    hooks:
      - id: detect-secrets
        args: ['--baseline', '.secrets.baseline']

  - repo: local
    hooks:
      - id: check-complexity
        name: Lizard cyclomatic complexity (CCN <= 12)
        entry: scripts/check-complexity.sh
        language: system
        pass_filenames: false
        stages: [pre-commit]
📄 Trecho scripts/check-security-sast.sh
#!/usr/bin/env bash
set -euo pipefail
cd "$(dirname "${BASH_SOURCE[0]}")/.."

# Skip gracieusement si opengrep absent en local ; fail-closed en CI.
if ! command -v opengrep >/dev/null 2>&1; then
  if [[ -n "${CI:-}" || -n "${GITHUB_ACTIONS:-}" ]]; then
    echo "opengrep introuvable en CI → fail-closed" >&2
    exit 1
  fi
  exit 0
fi

exec opengrep scan \
  --config=p/security-audit \
  --config=p/default \
  --config=p/python \
  --severity=ERROR \
  --error \
  --exclude=venv \
  --exclude=tests/fixtures \
  translate.py scripts/
📄 Trecho .github/workflows/sonarcloud.yml
name: SonarCloud

on:
  push:
    branches: [main]
  pull_request:
    types: [opened, synchronize, reopened]

jobs:
  sonarqube:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v6
        with:
          fetch-depth: 0

      - uses: actions/setup-python@v6
        with:
          python-version: '3.12'
          cache: pip

      - name: Install dependencies
        run: |
          python -m pip install --upgrade pip
          pip install -r requirements.txt
          pip install -r requirements-dev.txt
          pip install coverage

      - name: Run tests with coverage
        run: |
          coverage run --source=translate,scripts -m unittest discover tests
          coverage run --append --source=translate,scripts -m unittest discover scripts/tests
          coverage xml -o coverage.xml

      - name: SonarQube Scan
        uses: SonarSource/sonarqube-scan-action@v8
        env:
          SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}

Proteção 2 : a revisão assistida por IA + o fluxo multimodelo

Precisão de vocabulário : quando falo de Claude Opus nesta seção, estou falando do modelo que uso para desenvolver a v1.9 — não do modelo que o AI-Powered Markdown Translator usa para traduzir. O projeto em si suporta 4 provedores (OpenAI, Mistral AI, Claude, Gemini) e qualquer modelo (Sonnet, Haiku, Mistral Large, Gemini 3 Pro, etc.). No desenvolvimento, eu me fixo em Opus. No uso em execução, o projeto continua agnóstico.

Esquema conceitual : um humano orquestrador cercado por quatro agentes de IA distintos pelo seu papel, ligados por setas de coordenação

O fluxo de trabalho real: 4 modelos, 4 papéis (para desenvolver)

  • Claude Code em Opus, exclusivamente (anthropic) : execução principal. Lê o contexto, escreve o código, aplica as correções. Nada de Sonnet, nada de Haiku, nada de fast mode. Neste projeto, eu quero o modelo de alto nível todas as vezes — a ideia é simples: garantimos o melhor para buscar o melhor resultado possível.
  • OpenAI Codex como solução de fallback (fallback) : usado em dois casos específicos :
    • Quando o Opus falha em um tema (raro, mas acontece — por exemplo, em correções pedidas por agentes externos como Codacy ou SonarCloud, o Claude às vezes não converge e eu transfiro o assunto para o Codex para destravar)
    • Quando a janela de uso da Anthropic está saturada. O Codex permite não perder o ritmo enquanto espero o reset da quota.
  • GPT-5.5 reasoning extra-high (xhigh) : desafia os planos antes da execução. Antes de deixar o Claude Code atacar um tema, eu passo o plano para o GPT-5.5 em reasoning extra-high. Ele faz as perguntas certas, aponta os pontos cegos. Isso evita seguir na direção errada e depois ter que corrigir.
  • /pr-review-toolkit:review-pr (skill plugin Claude Code) : revisão antes do merge com agentes especializados (segurança, qualidade, testes, comentários, design de tipos). O skill roda na PR antes de eu fazer merge — é a última rede de segurança de IA antes que o código entre em main.

Nenhum desses modelos basta sozinho. Cada um desempenha um papel diferente — o executor de alto nível, o suplente de capacidade, o desafiador de planos, o revisor (reviewer) multilateral.

/pr-review-toolkit : o que eu não teria visto

Tudo. Eu não olho o código. O skill aponta tudo — bugs escondidos, problemas de segurança, incoerências nos testes, testes que passam mas não testam nada.

Na PR #2 (75 commits, 9 837 adições, 1 982 remoções, 58 arquivos), um humano sozinho teria pulado 80 % da PR por cansaço. O skill não pula nada. Ele lê cada diff, cada teste, cada comentário. E, acima de tudo, ele desafia — recusa os padrões que identifica como ruins e propõe alternativas.

O humano como maestro, não como músico

Meu papel cobre toda a cadeia — exceto a escrita do código. Eu faço os papéis de product manager (pensar nas features, priorizar, arbitrar), QA (testar em casos reais, validar visualmente o resultado), tech lead (desafiar os planos com GPT-5.5 reasoning extra-high), cliente final (julgar o resultado pela minha própria experiência de uso diário no blog). O único papel que eu não desempenho é o de codar. O resto sou eu.

Virei produtor, não músico.

Ao serviço do blog: ele se traduz sozinho (quase 1 800 traduções)

AI-Powered Markdown Translator gera o próprio README em 14 idiomas, e é ele que produz todas as versões em língua estrangeira dos conteúdos de jls42.org. Na prática: quase 1 800 versões traduzidas alimentam o blog (25 artigos + 4 projetos + 98 notícias de IA × 14 idiomas, fora as fontes em FR — ou seja, 1 778 versões no momento em que escrevo). Toda página que você navega aqui em um idioma diferente do francês passou por este projeto.

É dogfooding levado ao extremo — e isso coloca a tradução à prova no próprio artigo que fala de tradução. Se o que você lê em ar, hi ou ko está coerente, é porque a rede de segurança da Novidade 1 (validação pós-tradução) está funcionando; se a nota de tradução aparece corretamente no topo, é porque a Novidade 2 (nota multi-posicional) funciona; se as citações EN são preservadas nas versões linguísticas, é porque a Novidade 3 (modo --news) também funciona.

Balanço: pair-IA rigoroso, não pair-IA malfeito

Desenvolver no feeling tem má fama por bons motivos. É justamente contra eles que eu trabalho. Quatro lições concretas saem desta v1.9:

  1. Falhas silenciosas são o inimigo número um. A IA produz código que parece OK e passa pelos testes unitários. Validação sistemática do lado do cliente. E usar outra IA para reler a produção real, não apenas o código.

  2. Hooks pre-commit < 10 s, senão são contornados; pre-push podem levar 30 s+. A IA gosta de adicionar ferramentas sem considerar o custo. É preciso enquadrar manualmente, seja no plano, seja depois — o importante é que, no fim, os hooks estejam bem ajustados e realmente usados no dia a dia.

  3. Cobertura sem asserção forte = teatro. A IA pode gerar 200 testes que passam e não testam nada. unittest + asserções precisas > pytest com mocks aos montes. Verifique o valor retornado, não apenas que o código não travou.

  4. A revisão (PR review) por IA não é opcional. Quando o autor humano não releu, o revisor IA não é enfeite — é o olho delegado.

Fazer vibe coding direito também é aceitar que a gente não lê o código e delegar a leitura crítica a outras IAs que realmente o fazem.

O que este projeto revela

Esta v1.9 ilustra vários aspectos da minha forma de trabalhar:

  • O papel humano cobre toda a cadeia, exceto o código : produto (pensar nas features, priorizar), QA (testar em casos reais, validar visualmente), tech lead (desafiar os planos com um LLM em reasoning extra-high), cliente final (julgar pelo uso real). O único papel que eu não desempenho é o de codar.
  • Dobrar as redes de segurança, não removê-las : menos revisão humana = mais validação instrumentada. Compromisso assumido, não falta de rigor. Se eu removo a revisão, preciso dobrar as redes de segurança, não confiar cegamente na IA.
  • IA para descobrir os bugs da IA : a falha silenciosa foi encontrada pelo Claude durante os testes práticos multi-repo. Delegação completa : também é possível delegar a revisão crítica.
  • O pair-IA como multiplicador no tempo pessoal : levo este projeto nas noites e nos fins de semana. Sem pair-IA, claramente eu não iria tão longe nem tão rápido. Com ele, consigo manter um projeto open-source em nível industrial à margem das minhas outras obrigações. É isso que o vibe coding torna possível — não substituir o desenvolvedor, mas permitir que ele faça o que não conseguiria sozinho.
  • Iterar em vez de refazer tudo : 9 versões, refatoração incremental (1 função → 7 helpers), retrocompatibilidade preservada. O pair-IA ajuda a iterar rápido sem reescrever tudo.

Recursos


Se você quiser testar AI-Powered Markdown Translator nos seus próprios Markdown — README open-source, artigos de blog, documentação técnica —, o código está no GitHub. Instalação em poucos minutos, 4 providers suportados, modo --eco para reduzir o custo, modo --news para preservar as citações de origem e, agora, uma stack de qualidade v1.9 que você pode reutilizar como template para seus próprios projetos em pair-IA.

Se você desenvolve no feeling (vibe coding) seus projetos pessoais, não vá pelo caminho mais simples em termos de qualidade. Confiabilidade é o preço da velocidade — assuma os dois juntos.