# Research & Architectural Decisions: Deterministic Content Selection **Branch**: `004-deterministic-content-selection` | **Date**: 2026-08-20 | **Spec**: [spec.md](spec.md) --- ## 1. Text Normalization Pipeline ### Context Cada extrator (Trafilatura, Newspaper4k e Readability) gera textos com diferentes resíduos de formatação (entidades HTML como `&` ou ` `, links no formato markdown `[texto](url)` ou tags `texto`, variações de quebras de linha e pontuações). A comparação textual para consenso exige uma normalização uniforme, determinística e de alta performance. ### Decisions 1. **Decodificação de entidades HTML**: Utilizar `html.unescape()` da biblioteca padrão do Python. 2. **Remoção de imagens Markdown**: Expressão regular `re.compile(r'!\s*\[[^\]]*\]\([^)]*\)')` substituindo imagens Markdown por espaços para descartar marcação de mídia não-textual e evitar falsos consensos com legendas. 3. **Preservação de texto de links Markdown**: Expressão regular `re.compile(r'\[([^\]]+)\]\([^)]+\)')` substituindo links Markdown pelo texto âncora `\1`. 4. **Remoção de tags HTML**: Expressão regular `re.compile(r'<[^>]+>')` substituindo tags por espaços para evitar fusão acidental de palavras vizinhas. 5. **Normalização Unicode**: `unicodedata.normalize('NFKC', text)` para uniformizar caracteres compostos, ligaduras e variantes tipográficas. 6. **Conversão para minúsculas**: `.lower()` após NFKC. 7. **Colapso de espaços em branco**: `re.sub(r'\s+', ' ', text).strip()`. 8. **Tokenização**: Extração de sequências alfanuméricas com `re.findall(r'[\w]+', text, flags=re.UNICODE)`. Pontuações são descartadas naturalmente sem remoção semântica de palavras. ### Rationale - 100% implementável com módulos padrão do Python (`re`, `unicodedata`, `html`), garantindo portabilidade em qualquer ambiente sem novas dependências externas. - Complexidade linear $O(N)$ no tamanho do texto, com execução em frações de milissegundo por artigo. ### Alternatives Considered - `BeautifulSoup` para strip de tags: Rejeitado por ser mais lento e desnecessário para textos já extraídos. - `nltk` ou `spacy`: Rejeitados por adicionarem dependências pesadas, download de modelos e lentidão desnecessária para uma tarefa de tokenização alfanumérica pura. --- ## 2. 5-Token Shingles & Consensus Metrics ### Context O algoritmo compara a sobreposição textual entre os candidatos ativos através de janelas deslizantes consecutivas de 5 tokens (shingles). ### Decisions 1. **Geração de Shingles**: - Para um candidato com $T$ tokens ordenados $[t_0, t_1, \dots, t_{T-1}]$: - Se $T \ge 5$: conjunto de tuplas de 5 tokens $\{ (t_i, t_{i+1}, t_{i+2}, t_{i+3}, t_{i+4}) \mid 0 \le i \le T-5 \}$. - Se $1 \le T \le 4$: conjunto contendo uma única tupla com todos os tokens $\{ (t_0, \dots, t_{T-1}) \}$. - Se $T = 0$: conjunto vazio $\emptyset$. 2. **Construção do Consenso**: - Para cada shingle único observado nos candidatos ativos, conta-se em quantos candidatos distintos ele aparece. - $\text{Consenso} = \{ s \mid \text{contagem}(s) \ge 2 \}$. 3. **Métricas por Candidato Ativo $C$**: - $\text{cobertura}(C) = \frac{|C_{\text{shingles}} \cap \text{Consenso}|}{|\text{Consenso}|}$ - $\text{suporte}(C) = \frac{|C_{\text{shingles}} \cap \text{Consenso}|}{|C_{\text{shingles}}|}$ - $\text{score}(C) = \frac{2 \times \text{cobertura}(C) \times \text{suporte}(C)}{\text{cobertura}(C) + \text{suporte}(C)}$ (se denominador for zero, $\text{score} = 0.0$). ### Rationale - A métrica de pontuação $F_1$ penaliza tanto extratores que perderam conteúdo essencial (baixa cobertura) quanto extratores que trouxeram excesso de lixo/boilerplate do site (baixo suporte). - A representação por `set` de tuplas em Python permite operações de intersecção (`&`) com complexidade ótima de tempo $O(|C|)$. --- ## 3. Regras de Decisão, Empate Técnico e Desempate Hierárquico ### Context O sistema precisa garantir uma escolha única e determinística em todas as variações possíveis de entrada. ### Decisions 1. **Formação do Conjunto Ativo**: - Classificação: - `Usável`: `text` é string não vazia após normalização e `error` é `None`/vazio. - `Degradado`: `text` é string não vazia após normalização, mas `error` não é `None`. - `Indisponível`: `text` é nulo, ausente, não-string ou vazio. - Se houver $\ge 1$ Usável $\to$ Ativos = Usáveis. - Senão, se houver $\ge 1$ Degradado $\to$ Ativos = Degradados. - Senão $\to$ Seleciona `newspaper4k` diretamente (Fallback Final). - Se $|\text{Ativos}| = 1 \to$ Seleciona o único candidato ativo imediatamente. 2. **Seleção Com Consenso ($|\text{Consenso}| > 0$)**: - Maior score $S_{\max} = \max_{C \in \text{Ativos}} \text{score}(C)$. - Grupo de empate técnico: $\{ C \in \text{Ativos} \mid S_{\max} - \text{score}(C) \le 0.03 + 10^{-9} \}$. - Se grupo tiver 1 candidato $\to$ Seleciona ele. - Se grupo tiver $\ge 2$ candidatos $\to$ Seleciona o candidato com menor $|C_{\text{shingles}}|$ (menor conteúdo excedente). - Se ainda houver empate no número de shingles $\to$ Desempate por prioridade fixa: `newspaper4k` > `readability` > `trafilatura`. 3. **Seleção Sem Consenso ($|\text{Consenso}| = 0$)**: - Se $|\text{Ativos}| = 3 \to$ Seleciona candidato com quantidade **mediana** de shingles. - Se $|\text{Ativos}| = 2 \to$ Seleciona candidato com **maior** quantidade de shingles. - Se $|\text{Ativos}| = 1 \to$ Seleciona o único candidato. - Empates na quantidade de shingles $\to$ Prioridade fixa: `newspaper4k` > `readability` > `trafilatura`. ### Rationale - Total aderência às seções 7.1 a 7.6 do PRD. A tolerância de $10^{-9}$ evita imprecisões de ponto flutuante em comparações `<= 0.03`. --- ## 4. Estratégia de I/O Não Destrutiva e Escrita Atômica ### Context O processamento em lote deve preservar a ordem dos artigos e todos os campos originais do JSON, gravando o resultado sem risco de corrupção de arquivos em caso de interrupção. ### Decisions 1. **Entrada e Saída**: - Nome padrão de saída: `_selected.json`. - Suporte a argumento opcional de saída `--output / -o`. 2. **Gravação Atômica**: - Gravar os dados em um arquivo temporário no mesmo diretório (`.tmp.`). - Executar substituição atômica via `os.replace(temp_path, target_path)`. 3. **Preservação de Conteúdo**: - Carregar o JSON original em estruturas nativas de dicionário/lista. - Inserir a chave `selected_extractor` diretamente em cada dicionário de artigo. - Se `selected_extractor` já existir na entrada, sobrescrever com o novo valor recalculado. ### Rationale - Garante integridade absoluta dos dados contra falhas de disco ou encerramentos abruptos.