feat(runtime): implement single-article consolidation runtime and modularize codebase

This commit is contained in:
2026-08-24 00:14:07 -03:00
parent e1e0be1353
commit 23de7d8fe7
176 changed files with 266754 additions and 10179 deletions
@@ -0,0 +1,698 @@
# Documento de Arquitetura — Runtime de consolidação de artigos
**Versão:** 1.0
**Status:** proposta aceita para implementação
**Data:** 23 de agosto de 2026
**Relacionado:** PRD Runtime de consolidação e higienização de artigos
**Regra mestre:** atender integralmente aos requisitos com arquitetura production-ready e a menor quantidade necessária de código, dependências, abstrações e componentes.
## 1. Propósito
Definir a arquitetura mínima e production-ready do runtime que recebe um artigo extraído por três bibliotecas, um ECP obrigatório e produz manifesto JSON e, quando aplicável, Markdown editorial fundamentado.
Este documento não especifica self-healing. O runtime apenas preserva a telemetria necessária para um subprojeto futuro.
## 2. Direcionadores
1. Atender 100% dos requisitos do PRD.
2. Usar o menor número possível de componentes e dependências.
3. Manter o fluxo explícito, testável e auditável.
4. Não usar regex em processamento textual.
5. Não usar palavras-chave manuais para decisões semânticas multilíngues.
6. Não permitir geração livre do artigo pelo LLM.
7. Executar LLM de higienização para todo artigo.
8. Exigir ECP antes de Markdown.
9. Usar somente modelos baratos no runtime.
10. Permitir troca de provider e modelo por configuração certificada.
11. Suportar 100 artigos por hora sem perda ou duplicação.
## 3. Limites do sistema
### 3.1 Entrada
- um objeto de artigo já contendo `selected_extractor`;
- um ECP Snapshot válido;
- configuração versionada.
O artigo já deve ter sido considerado conteúdo textual elegível pelo processo anterior. O runtime não classifica predominância de vídeo ou galeria.
### 3.2 Saída
- um resultado JSON estruturado por invocação;
- um manifesto JSON persistido quando houver fingerprint estabelecido;
- zero ou um arquivo Markdown;
- estado persistido;
- telemetria enviada ou preservada para reenvio.
### 3.3 Dependências externas
- provider LLM primário barato;
- provider LLM fallback barato;
- classificador ECP existente;
- Langfuse;
- filesystem;
- SQLite.
Promptfoo participa do CI e não do processo online.
## 4. Visão de contexto
```mermaid
flowchart LR
O["Orquestrador"] --> R["Runtime CLI"]
R --> L["Providers LLM baratos"]
R --> E["Classificador ECP"]
R --> S["Manifesto e Markdown"]
R -. telemetria .-> F["Langfuse"]
```
## 5. Arquitetura lógica
```mermaid
flowchart TD
A["Validação e fingerprint"] --> B["Parsing estrutural e candidatos"]
B --> C["Higienização e reparos"]
C --> D["Gate ECP"]
D --> E{"ECP aprovado?"}
E -- Não --> F["Manifesto rejeitado"]
E -- Sim --> G["Enriquecimento e Markdown"]
```
## 6. Componentes
| Componente | Responsabilidade | Não faz |
| --- | --- | --- |
| CLI | Carregar entradas, configuração e coordenar uma execução | Processar lotes internamente |
| Contract Validator | Validar artigo, ECP e configuração | Inferir valores ausentes |
| Fingerprint Service | Gerar identidade idempotente | Deduzir identidade editorial |
| Structural Parser | Parsear HTML, Markdown, JSON-LD, URLs e Unicode | Interpretar intenção editorial |
| Candidate Builder | Criar IDs, origem, posição e equivalências | Escolher conteúdo final |
| Content Hygiene | Selecionar blocos, metadados, links, imagens e reparos | Sentimento, tags ou ECP |
| Repair Validator | Validar e aplicar pequenos reparos | Autorizar reescrita livre |
| Markdown Assembler | Construir documento intermediário e final | Criar conteúdo |
| ECP Adapter | Invocar classificador e normalizar resultado | Alterar artigo |
| Enrichment | Gerar sentimento e tags | Alterar o corpo |
| Model Gateway | Executar primário, retry técnico e fallback | Escolher modelo caro |
| State Store | Idempotência, estado, saídas e telemetria pendente | Armazenar segredos |
| Langfuse Adapter | Instrumentar traces e generations | Bloquear saída válida quando indisponível |
## 7. Orquestração
### 7.1 Escolha
O runtime será uma máquina de estados explícita em Python. Não usará LangChain, LangGraph, agente, planner ou framework de workflow.
### 7.2 Justificativa
LangGraph foi avaliado como motor standalone de workflow com estado, e não apenas como framework para agentes. A existência de etapas determinísticas, chamadas de LLM, estados persistidos e arestas condicionais torna seu uso tecnicamente possível, mas não o torna necessário.
O runtime atual possui:
- sequência predeterminada;
- poucas bifurcações, todas conhecidas e controladas pela aplicação;
- nenhuma escolha autônoma de ferramentas ou etapas pelo modelo;
- nenhum ciclo semântico entre etapas;
- nenhuma pausa aguardando intervenção humana ou evento externo;
- uma unidade de artigo por execução curta;
- SQLite necessário para idempotência, reconciliação, erros e escrita atômica, independentemente do mecanismo de orquestração;
- retry técnico e fallback com políticas limitadas e conhecidas.
Nesse cenário, LangGraph não substituiria o gateway de modelos, os prompts, os schemas, o harness, as validações, o state store, a observabilidade ou os testes. Ele substituiria apenas uma pequena camada de transições já representável de forma direta em Python, adicionando dependência e semântica operacional próprias sem eliminar código relevante.
LangChain também não é necessário: as chamadas são delimitadas, não existem agentes ou tool calling, e o gateway agnóstico encapsula diretamente as diferenças entre providers.
Langfuse e Promptfoo são independentes dessa decisão. Langfuse permanece responsável pela observabilidade online, e Promptfoo pelos evals e gates fora do runtime.
### 7.3 Estados persistidos
| Estado | Significado |
| --- | --- |
| `received` | Entradas carregadas |
| `validated` | Artigo, ECP e configuração válidos |
| `content_cleaned` | Conteúdo textual e reparos validados |
| `ecp_approved` | ECP direto ou contextual |
| `ecp_rejected` | ECP tangencial ou não relacionado |
| `enriched` | Sentimento e tags válidos |
| `completed_text` | Manifesto e Markdown gravados |
| `failed` | Falha terminal com código |
Cada transição deve registrar início, fim, duração e resultado. Reexecução parte do último estado seguro ou retorna a saída concluída quando o fingerprint e as versões forem idênticos.
### 7.4 Critérios de reavaliação
A decisão deve ser reavaliada somente se surgir requisito concreto que não seja atendido com simplicidade pela orquestração atual, como:
- ciclos semânticos que retornem a etapas anteriores;
- pausa e retomada aguardando aprovação humana ou evento externo;
- roteamento dinâmico de etapas decidido por modelo;
- coordenação de agentes ou subfluxos dinâmicos;
- execução longa em que checkpoint por etapa reduza materialmente custo ou perda de trabalho;
- capacidade do framework substituir persistência ou controle próprio relevante, em vez de duplicá-lo.
A adição de uma nova bifurcação predeterminada, isoladamente, não justifica introduzir um framework de workflow.
## 8. Organização de módulos
A implementação deve usar módulos coesos, sem criar camadas genéricas adicionais:
- contratos;
- parsing estrutural;
- candidatos;
- higienização;
- reparos;
- ECP;
- enriquecimento;
- renderização;
- gateway LLM;
- estado;
- observabilidade;
- CLI.
Essa lista representa responsabilidades, não exige um pacote ou classe por item. Responsabilidades pequenas podem compartilhar módulo quando isso reduzir código sem misturar regras.
## 9. Política de dependências
### 9.1 Preferência
1. biblioteca padrão;
2. dependência já existente no monorepo;
3. biblioteca consolidada que elimine implementação própria relevante;
4. código próprio somente quando a regra for específica do produto.
### 9.2 Aprovação
Toda nova dependência deve registrar:
- requisito atendido;
- alternativa da biblioteca padrão;
- impacto de segurança e manutenção;
- licença;
- impacto de tamanho e inicialização.
### 9.3 Proibições
- framework de agentes;
- biblioteca apenas para uma função trivial;
- segundo schema do ECP;
- parser manual de HTML, Markdown ou URL;
- regex para texto;
- dependência antecipada de self-healing.
## 10. Contratos versionados
Devem possuir versão independente:
- artigo de entrada;
- ECP Snapshot referenciado;
- configuração do runtime;
- candidatos enviados ao LLM;
- resposta de higienização;
- operações de reparo;
- resposta de enriquecimento;
- manifesto de saída;
- prompts.
Mudanças incompatíveis exigem nova versão e eval completo.
## 11. Validação inicial
Ordem obrigatória:
1. parse JSON;
2. validar schema do artigo;
3. validar schema do ECP na fonte canônica;
4. validar `selected_extractor`;
5. validar presença mínima de URL, título e conteúdo textual;
6. validar configuração certificada dos modelos;
7. calcular fingerprint;
8. consultar idempotência.
Nenhum provider, Langfuse remoto ou classificador é chamado antes das validações locais que podem encerrar a execução.
## 12. Fingerprint e idempotência
O fingerprint deve combinar, por serialização canônica e hash:
- identidade do artigo;
- hashes das três extrações;
- `selected_extractor`;
- identidade e versão do ECP;
- versão dos prompts;
- configuração funcional do runtime;
- modelos configurados.
Dados operacionais que não alteram o resultado, como trace ID e timestamp, não entram no fingerprint.
SQLite mantém:
- fingerprint;
- estado atual;
- timestamps;
- caminhos de saída;
- hashes dos arquivos;
- versões funcionais;
- erro terminal;
- telemetria pendente.
Para concorrência compatível com o volume, usar transações curtas, modo WAL e timeout de bloqueio configurado. Não adicionar banco servidor sem evidência de necessidade.
## 13. Parsing estrutural sem regex
### 13.1 HTML
Usar parser DOM. Navegação, elementos e atributos são inspecionados pela árvore, nunca por manipulação de strings com regex.
### 13.2 Markdown
Usar parser CommonMark/AST para headings, parágrafos, links, imagens, listas, citações e ênfase.
### 13.3 JSON-LD e metadados
Usar parser JSON e navegação por objetos. Tipos estruturados são evidência, não decisão semântica completa.
### 13.4 URLs
Usar parser de URL. Validar esquema, host e componentes sem regex.
### 13.5 Texto
Usar normalização Unicode, segmentador e tokenizador multilíngue. Comparações de sequência, distância e similaridade devem operar sobre estruturas produzidas por essas bibliotecas.
### 13.6 Enforcement
O CI deve usar AST de Python para impedir importação ou chamada direta de engine de regex nos módulos do pipeline textual. Dependências transitivas não são inspecionadas internamente, mas nenhuma API de regex pode ser usada pelo código do projeto ou por assertions configuradas.
## 14. Modelo de candidatos
Cada candidato contém:
- `candidate_id` estável na execução;
- tipo;
- extrator;
- campo;
- texto ou URL original;
- representação estrutural;
- índice de ordem;
- parent ID, quando aplicável;
- equivalências;
- hash;
- flags puramente estruturais.
IDs devem ser opacos ao LLM. A numeração não deve incorporar julgamento de qualidade.
### 14.1 Ordem
O `selected_extractor` fornece a espinha dorsal da ordem. Blocos equivalentes dos outros extratores oferecem representações alternativas e evidência de consenso.
Blocos exclusivos de outro extrator podem ser candidatos, mas só entram no resultado por seleção explícita do LLM e validação de grounding.
### 14.2 Equivalência
Equivalência pode usar:
- igualdade após normalização Unicode e whitespace por biblioteca;
- tokenização multilíngue;
- similaridade de sequência;
Não usar regex nem dicionários de palavras.
## 15. Higienização extrativa
### 15.1 Interface
O LLM recebe candidatos e devolve decisões estruturadas. Ele não deve devolver o artigo completo.
Resposta mínima:
- IDs de título, subtítulo e autor escolhidos;
- IDs de blocos mantidos;
- IDs de imagens comuns mantidas;
- IDs de links mantidos;
- lista de reparos;
- motivo categórico para blocos removidos, quando exigido pelo harness.
### 15.2 Garantia de grounding
O harness:
1. valida o schema;
2. valida existência e tipo dos IDs;
3. recupera conteúdo somente do mapa interno;
4. aplica reparos válidos;
5. monta o Markdown;
6. confirma que URLs e imagens pertencem à entrada.
URL de origem e data de publicação são resolvidas deterministicamente antes da chamada. O LLM não as escolhe nem corrige.
O LLM nunca controla diretamente o renderer ou o filesystem.
### 15.3 Fallback
- Falha técnica: retry limitado conforme política.
- Schema ou grounding inválido: fallback barato.
- Falha do fallback: usar seleção determinística conservadora apenas quando ela cumprir todos os requisitos de grounding e conteúdo mínimo.
- Se não houver fallback seguro: `HYGIENE_FAILED`.
O fallback determinístico não usa regex nem tenta remover semanticamente publicidade. Ele preserva a base do `selected_extractor`, elimina apenas elementos estruturalmente inválidos e pode resultar em falha quando não houver garantia de qualidade.
## 16. Pequenos reparos
### 16.1 Representação
Cada operação contém:
- ID alvo;
- fragmento original exato;
- substituição;
- categoria fechada;
- justificativa.
### 16.2 Validação
O harness deve:
- confirmar que o fragmento original pertence ao candidato;
- exigir ocorrência inequívoca ou referência estrutural suficiente;
- comparar original e substituição com biblioteca Unicode/tokenização;
- impedir alterações em entidades sensíveis como números, datas, placares, nomes e citações, salvo quando o defeito for exclusivamente de codificação comprovável;
- registrar diff e decisão;
- descartar apenas o reparo inválido e manter o original.
Os limites quantitativos de similaridade devem ser calibrados na golden set. Não podem ser inventados no código sem evidência de eval.
### 16.3 Responsabilidade do prompt
O prompt define expressamente que reparo não autoriza estilo, sinônimo, fluência, paráfrase ou correção factual.
## 17. Montagem do Markdown intermediário
O assembler:
1. recupera metadados escolhidos;
2. recupera blocos mantidos;
3. aplica reparos validados;
4. preserva a ordem canônica;
5. materializa links escolhidos;
6. insere imagens comuns em posições fundamentadas;
7. remove duplicação estrutural exata de título/subtítulo;
8. serializa Markdown sem HTML inline necessário.
Esse documento é a entrada do ECP.
## 18. Integração ECP
### 18.1 Adapter
O runtime usa o contrato público do classificador ECP existente. Não replica regras, embeddings ou schema.
### 18.2 Entrada
Entrada do ECP: Markdown intermediário higienizado.
### 18.3 Saída
O adapter exige categoria, `is_inherent`, confiança, rationale e evidências conforme contrato do classificador.
### 18.4 Gate
- direto/contextual: prosseguir;
- tangencial/não relacionado: manifesto rejeitado, sem Markdown;
- falha: estado terminal, sem Markdown.
Qualquer tier LLM utilizado pelo classificador ECP durante esta execução deve estar configurado com modelo barato certificado. O adapter deve registrar essa generation e impedir configuração potente no runtime.
## 19. Enriquecimento
Uma chamada separada recebe somente:
- título final;
- subtítulo, se houver;
- corpo final;
- idioma;
- identidade mínima do ECP;
- schema.
Retorna sentimento relativo ao ECP, 3 a 8 tags e IDs de evidência. Não pode alterar o corpo.
Falha do primário chama fallback barato. Falha dos dois impede Markdown porque sentimento e tags são campos obrigatórios do contrato final.
## 20. Model Gateway
### 20.1 Interface mínima
O gateway aceita:
- papel lógico;
- mensagens/contexto;
- schema estruturado;
- timeout;
- metadados de trace.
Retorna:
- saída estruturada;
- provider e modelo efetivos;
- tokens, custo e latência;
- status técnico;
- tentativa e fallback.
### 20.2 Configuração
Cada papel mapeia para uma configuração certificada:
- `runtime_primary`;
- `runtime_fallback`.
Defaults inicialmente aprovados podem ser Groq com `openai/gpt-oss-20b` e DeepSeek com `deepseek-v4-flash`, sem acoplamento no domínio. A configuração efetiva somente entra em produção após Promptfoo.
### 20.3 Retry
Retry no mesmo provider é permitido apenas para:
- timeout;
- conexão interrompida;
- 429;
- 5xx;
- resposta vazia por falha técnica.
Não repetir no mesmo modelo para buscar decisão semântica diferente.
## 21. Prompts
Prompts ficam em arquivos versionados no repositório. Cada um possui:
- nome;
- versão semântica;
- hash;
- schema associado;
- casos Promptfoo correspondentes.
Prompts mínimos:
- higienização e reparos;
- sentimento e tags.
O mesmo arquivo é usado no runtime e no Promptfoo. Langfuse recebe a referência, mas não é a fonte primária nesta versão.
## 22. Renderer e arquivos
### 22.1 Nomes
Usar fingerprint no nome para evitar colisões e problemas de Unicode:
- `<fingerprint>.result.json`;
- `<fingerprint>.md`, quando aplicável.
### 22.2 Escrita atômica
1. renderizar em arquivo temporário no mesmo filesystem;
2. flush e fechamento;
3. validar conteúdo e hash;
4. renomear atomicamente para o destino;
5. persistir estado concluído na mesma unidade lógica.
Se manifesto e Markdown forem necessários, nenhum deles pode ser apresentado como concluído enquanto o par não estiver consistente.
## 23. Observabilidade
### 23.1 Hierarquia
- uma execução CLI: um `run_id`;
- um artigo: uma trace estável;
- cada etapa: span;
- cada tentativa LLM: generation.
Nomes de spans não incluem modelo, provider, URL ou IDs dinâmicos.
### 23.2 Conteúdo
Registrar por padrão contexto normalizado e respostas estruturadas, nunca secrets, headers ou variáveis de ambiente.
HTML bruto e JSON integral não devem ser duplicados no Langfuse. Usar hashes e recortes necessários à depuração.
Deve existir configuração para não enviar conteúdo textual, preservando métricas e hashes.
### 23.3 Degradação
Se Langfuse falhar:
- continuar processamento;
- persistir evento mínimo pendente no SQLite;
- registrar log local estruturado;
- tentar flush no encerramento;
- permitir reenvio posterior pela mesma ferramenta operacional, sem serviço adicional.
## 24. Logging
Logs estruturados em JSON devem conter:
- timestamp;
- nível;
- `run_id`;
- fingerprint;
- estado;
- evento;
- código de erro;
- duração;
- provider/modelo/prompt quando aplicável;
- sem conteúdo sensível por padrão.
Exceções devem manter stack trace no log técnico, sem expor secrets.
## 25. Segurança e privacidade
- secrets somente por mecanismo de configuração seguro do ambiente;
- nenhuma chave em CLI, arquivo de saída ou log;
- permissões mínimas para diretórios;
- URLs de entrada tratadas como dados, não executadas;
- HTML e texto são conteúdo não confiável;
- instruções contidas no artigo não podem alterar o prompt;
- structured output e seleção por IDs limitam prompt injection;
- dependências fixadas e verificadas pelo processo do repositório.
## 26. Capacidade e concorrência
O runtime processa uma unidade por execução. O paralelismo é responsabilidade do orquestrador externo.
O componente deve permitir execuções concorrentes sobre o mesmo state store sem:
- duplicidade;
- corrupção;
- perda de estado;
- sobrescrita parcial.
O teste de staging deve sustentar 100 artigos por hora no perfil real de chamadas. Custo e latência são medidos; limites numéricos são aprovados antes do go-live com base nessa execução.
## 27. Falhas e recuperação
| Falha | Tratamento |
| --- | --- |
| Artigo ou ECP inválido | Encerrar antes de LLM |
| `selected_extractor` inválido | Encerrar sem recalcular |
| Provider primário indisponível | Retry técnico e fallback barato |
| Resposta sem grounding | Rejeitar e usar fallback barato |
| Reparo inválido | Preservar original e registrar |
| ECP indisponível ou inválido | Encerrar sem Markdown |
| Enriquecimento indisponível | Encerrar sem Markdown |
| Escrita interrompida | Limpar temporário seguro e retomar idempotentemente |
| Langfuse indisponível | Persistir telemetria pendente e continuar |
## 28. Deploy e configuração
O runtime deve ser empacotável de forma reproduzível e executar como processo CLI efêmero.
Configurações por ambiente:
- caminhos de entrada, saída e estado;
- endpoints e credenciais dos providers;
- primário e fallback;
- timeouts e retries técnicos;
- versões de prompt;
- Langfuse;
- política de conteúdo em traces;
- limites de concorrência do state store.
Configurações funcionais versionadas entram no fingerprint. Secrets e parâmetros puramente operacionais não entram.
## 29. CI/CD
Ordem mínima:
1. validação de formatos;
2. lint e análise estática;
3. verificação AST da proibição de regex;
4. testes unitários;
5. testes de contrato;
6. testes de integração com providers simulados;
7. Promptfoo reduzido;
8. build do pacote;
9. golden set completo antes da promoção;
10. staging com teste de 100 artigos/hora;
11. aprovação dos limites de custo e latência;
12. promoção controlada.
## 30. Decisões de simplicidade
- SQLite em vez de banco servidor enquanto atender concorrência e recuperação.
- filesystem em vez de object storage dentro do runtime.
- CLI em vez de API.
- orquestração direta em vez de framework de workflow sem requisito comprovado.
- dois adapters de providers em vez de roteador inteligente.
- prompts no repositório em vez de plataforma remota como fonte primária.
- telemetria pendente na mesma persistência em vez de fila adicional.
Cada escolha deve ser revisitada somente diante de requisito ou evidência operacional nova.
## 31. Preparação mínima para self-healing futuro
O runtime registra:
- prompt, modelo, provider e versões;
- falhas categorizadas;
- respostas estruturadas;
- scores;
- custos e latências;
- trace ID;
- hashes e evidências.
Não cria:
- serviço de análise;
- fila de candidatos;
- otimizador;
- judge;
- prompt candidato;
- promoção;
- canário;
- alerta de self-healing.
## 32. Riscos arquiteturais
| Risco | Mitigação mínima |
| --- | --- |
| LLM remover conteúdo válido | Golden set por blocos e fallback |
| LLM reescrever ao reparar | Operações de reparo, diff, prompt e eval específicos |
| Multilinguismo | NLP apropriado e eval estratificado por idioma |
| Regex ou keyword rule introduzida | ADR, revisão e teste AST |
| Provider indisponível | Retry técnico e fallback barato |
| Mudança de modelo alterar comportamento | Promptfoo e configuração certificada |
| Langfuse indisponível | Persistência de telemetria pendente |
| SQLite tornar-se gargalo | Medição; migrar apenas com evidência |
## 33. Critérios arquiteturais de aceite
1. Fluxo predeterminado implementado como máquina de estados explícita, com bifurcações controladas.
2. Nenhuma dependência de LangChain, LangGraph ou agente.
3. Nenhum uso de regex no pipeline textual ou assertions.
4. Nenhum dicionário manual decide semântica multilíngue.
5. LLM retorna decisões e IDs, não o artigo completo.
6. Reparos são operações auditáveis e reversíveis.
7. ECP bloqueia Markdown quando não inerente.
8. Runtime conhece somente modelos baratos configurados por papel.
9. Escrita e retomada são idempotentes.
10. Langfuse pode falhar sem perder resultado ou telemetria mínima.
11. Promptfoo não é dependência online.
12. Teste de 100 artigos/hora passa sem perda ou duplicação.
13. Limites de custo e latência são definidos antes do go-live a partir da baseline.
14. Nenhum componente de self-healing é implantado.