280 lines
15 KiB
Markdown
280 lines
15 KiB
Markdown
# PRD — Seleção determinística da biblioteca de extração de conteúdo
|
||
|
||
**Versão:** 1.0
|
||
**Data:** 20/08/2026
|
||
**Status:** Pronto para implementação
|
||
|
||
## 1. Contexto
|
||
|
||
Cada artigo é processado pelas bibliotecas Trafilatura, Newspaper4k e Readability a partir do mesmo HTML. O resultado é consolidado em um arquivo JSON que contém, para cada artigo, as saídas das três bibliotecas.
|
||
|
||
É necessário escolher deterministicamente uma única biblioteca por artigo. A escolha deve ocorrer mesmo quando todos os resultados forem ruins. O processamento não pode retornar estado ambíguo nem deixar um artigo sem seleção.
|
||
|
||
## 2. Objetivo
|
||
|
||
Receber um arquivo JSON no formato do arquivo de referência, selecionar a melhor saída de extração disponível para cada artigo e gerar um novo arquivo JSON com os mesmos dados, acrescentando somente a chave `selected_extractor` em cada item de `articles`.
|
||
|
||
## 3. Escopo
|
||
|
||
### 3.1 Incluído
|
||
|
||
- Ler um arquivo JSON com uma coleção `articles`.
|
||
- Comparar as saídas de Trafilatura, Newspaper4k e Readability de cada artigo.
|
||
- Escolher obrigatoriamente uma das três bibliotecas.
|
||
- Adicionar `selected_extractor` em cada artigo.
|
||
- Gerar um novo arquivo JSON.
|
||
- Preservar os dados e a ordem dos artigos recebidos.
|
||
|
||
### 3.2 Fora do escopo
|
||
|
||
- Baixar ou renderizar páginas.
|
||
- Verificar se as bibliotecas receberam o mesmo HTML.
|
||
- Executar novamente as bibliotecas de extração.
|
||
- Limpar, recortar, combinar ou reescrever o conteúdo extraído.
|
||
- Gerar Markdown.
|
||
- Usar LLM, embeddings ou regras específicas por domínio.
|
||
- Alterar qualquer campo existente no JSON.
|
||
- Adicionar métricas, justificativas ou outras chaves ao arquivo de saída.
|
||
|
||
## 4. Premissas
|
||
|
||
- As três bibliotecas processaram exatamente o mesmo HTML.
|
||
- A entrada segue a estrutura do JSON de referência.
|
||
- A seleção é executada individualmente para cada artigo.
|
||
- O algoritmo deve sempre produzir uma escolha, inclusive em situações sem concordância entre as bibliotecas.
|
||
|
||
## 5. Entrada
|
||
|
||
### 5.1 Arquivo
|
||
|
||
- Formato: JSON válido.
|
||
- A raiz deve conter `articles` como uma lista.
|
||
- Cada item de `articles` representa um artigo.
|
||
|
||
### 5.2 Conteúdos comparados
|
||
|
||
| Biblioteca | Campo usado na comparação |
|
||
| ----------- | -------------------------- |
|
||
| Trafilatura | `trafilatura.text` |
|
||
| Newspaper4k | `newspaper4k.text` |
|
||
| Readability | `readability.cleaned_text` |
|
||
|
||
Os demais campos, incluindo título, descrição, resumo, palavras-chave, HTML estruturado e metadados, não participam da seleção.
|
||
|
||
### 5.3 Estado de um candidato
|
||
|
||
Para cada biblioteca, o candidato é classificado em um dos seguintes estados:
|
||
|
||
| Estado | Condição |
|
||
| ------------ | ---------------------------------------------------------------------------------- |
|
||
| Utilizável | Campo de conteúdo é uma string não vazia após normalização e `error` é nulo |
|
||
| Degradado | Campo de conteúdo é uma string não vazia após normalização, mas `error` não é nulo |
|
||
| Indisponível | Campo ausente, nulo, de tipo diferente de string ou vazio após normalização |
|
||
|
||
O campo de erro considerado é `trafilatura.error`, `newspaper4k.error` ou `readability.error`, conforme a biblioteca.
|
||
|
||
## 6. Saída
|
||
|
||
### 6.1 Arquivo
|
||
|
||
O arquivo de entrada nunca deve ser alterado. Deve ser criado um novo arquivo com o nome:
|
||
|
||
`<nome_original_sem_extensão>_selected.json`
|
||
|
||
Exemplo: `river_plate_extracted(2).json` gera `river_plate_extracted(2)_selected.json`.
|
||
|
||
### 6.2 Alteração permitida
|
||
|
||
Cada item de `articles` deve receber exatamente uma nova chave no mesmo nível de `trafilatura`, `newspaper4k` e `readability`:
|
||
|
||
`selected_extractor`
|
||
|
||
Valores permitidos:
|
||
|
||
- `trafilatura`
|
||
- `newspaper4k`
|
||
- `readability`
|
||
|
||
Não são permitidos `null`, string vazia, `ambiguous` ou qualquer outro valor.
|
||
|
||
### 6.3 Preservação da entrada
|
||
|
||
- Todas as chaves e valores existentes devem permanecer semanticamente idênticos.
|
||
- A ordem dos itens de `articles` deve ser preservada.
|
||
- Nenhum artigo pode ser adicionado ou removido.
|
||
- Espaçamento, indentação e ordem textual das chaves do JSON não fazem parte do contrato, pois o arquivo pode ser serializado novamente.
|
||
- Caso `selected_extractor` já exista, seu valor deve ser recalculado e substituído.
|
||
|
||
## 7. Algoritmo determinístico de seleção
|
||
|
||
### 7.1 Formar o conjunto ativo
|
||
|
||
Para cada artigo:
|
||
|
||
1. Identificar os candidatos utilizáveis.
|
||
2. Se existir pelo menos um utilizável, considerar somente os utilizáveis.
|
||
3. Se não existir utilizável, considerar os candidatos degradados.
|
||
4. Se não existir candidato utilizável nem degradado, selecionar `newspaper4k` pelo desempate final obrigatório.
|
||
5. Se o conjunto ativo possuir somente um candidato, selecioná-lo imediatamente.
|
||
|
||
### 7.2 Normalizar os conteúdos
|
||
|
||
A normalização serve apenas para comparação e não modifica o JSON de saída.
|
||
|
||
Para cada candidato ativo:
|
||
|
||
1. Decodificar entidades HTML.
|
||
2. Remover marcação HTML e Markdown, preservando o texto visível.
|
||
3. Em links, preservar o texto e remover o endereço.
|
||
4. Aplicar normalização Unicode NFKC.
|
||
5. Converter o texto para minúsculas.
|
||
6. Substituir toda sequência de espaços, tabulações ou quebras de linha por um único espaço.
|
||
7. Tokenizar mantendo letras e números Unicode.
|
||
8. Desconsiderar pontuação.
|
||
|
||
Nenhuma palavra ou trecho pode ser removido por interpretação semântica.
|
||
|
||
### 7.3 Gerar shingles
|
||
|
||
- Gerar a sequência ordenada de tokens de cada candidato.
|
||
- Formar o conjunto de todas as janelas consecutivas de cinco tokens.
|
||
- Quando o candidato possuir entre um e quatro tokens, usar a sequência completa como um único shingle.
|
||
- Candidato sem token é indisponível e não participa do conjunto ativo.
|
||
|
||
### 7.4 Construir o consenso
|
||
|
||
O consenso é o conjunto de shingles presentes em pelo menos dois candidatos ativos.
|
||
|
||
Para cada candidato ativo, calcular:
|
||
|
||
**Cobertura:**
|
||
|
||
`coverage = quantidade de shingles do consenso presentes no candidato / quantidade total de shingles do consenso`
|
||
|
||
**Suporte:**
|
||
|
||
`support = quantidade de shingles do candidato presentes no consenso / quantidade total de shingles do candidato`
|
||
|
||
**Pontuação:**
|
||
|
||
`score = 2 × coverage × support / (coverage + support)`
|
||
|
||
Quando `coverage + support` for zero, a pontuação será zero.
|
||
|
||
### 7.5 Selecionar quando existe consenso
|
||
|
||
1. Ordenar os candidatos por `score`, do maior para o menor.
|
||
2. Identificar o maior `score`.
|
||
3. Considerar empate técnico todo candidato cuja diferença para o maior `score` seja menor ou igual a `0,03`.
|
||
4. Se houver apenas um candidato no empate técnico, selecioná-lo.
|
||
5. Se houver empate técnico, selecionar o candidato com a menor quantidade de shingles.
|
||
6. Se a quantidade de shingles também empatar, aplicar a prioridade final:
|
||
1. `newspaper4k`
|
||
2. `readability`
|
||
3. `trafilatura`
|
||
|
||
A preferência pelo menor candidato ocorre somente no empate técnico. Nesse cenário, os candidatos possuem qualidade de concordância equivalente, e a decisão favorece menor conteúdo excedente.
|
||
|
||
### 7.6 Selecionar quando não existe consenso
|
||
|
||
Quando nenhum shingle aparece em pelo menos dois candidatos ativos:
|
||
|
||
- Com três candidatos ativos: selecionar o candidato com a quantidade mediana de shingles.
|
||
- Com dois candidatos ativos: selecionar o candidato com a maior quantidade de shingles.
|
||
- Com um candidato ativo: selecionar o único candidato.
|
||
- Em empate de quantidade: aplicar a prioridade `newspaper4k`, `readability`, `trafilatura`.
|
||
- Sem candidato ativo: selecionar `newspaper4k`.
|
||
|
||
Essas regras garantem uma escolha mesmo quando não existe concordância textual.
|
||
|
||
### 7.7 Gravar a escolha
|
||
|
||
Adicionar ou substituir `selected_extractor` no artigo com o identificador da biblioteca vencedora. Repetir o processo até que todos os itens de `articles` tenham sido processados.
|
||
|
||
## 8. Requisitos funcionais
|
||
|
||
| ID | Requisito |
|
||
| ------ | ------------------------------------------------------------------------------------------------------- |
|
||
| FR-001 | O sistema deve aceitar um arquivo JSON como entrada. |
|
||
| FR-002 | O sistema deve validar que a raiz é um objeto e que `articles` é uma lista. |
|
||
| FR-003 | O sistema deve processar todos os artigos, preservando sua ordem. |
|
||
| FR-004 | O sistema deve usar exclusivamente os três campos de conteúdo definidos no PRD para calcular a escolha. |
|
||
| FR-005 | O sistema deve executar a normalização e a comparação conforme o algoritmo deste PRD. |
|
||
| FR-006 | O sistema deve selecionar exatamente uma biblioteca por artigo. |
|
||
| FR-007 | O sistema nunca deve produzir resultado ambíguo. |
|
||
| FR-008 | O sistema deve adicionar somente `selected_extractor` em cada artigo. |
|
||
| FR-009 | O valor de `selected_extractor` deve pertencer ao catálogo fechado de valores permitidos. |
|
||
| FR-010 | O sistema deve preservar todas as chaves e valores recebidos. |
|
||
| FR-011 | O sistema deve gerar um novo arquivo e manter o arquivo original inalterado. |
|
||
| FR-012 | O sistema deve produzir a mesma seleção sempre que receber exatamente a mesma entrada. |
|
||
| FR-013 | O sistema deve recalcular `selected_extractor` quando a chave já existir. |
|
||
|
||
## 9. Tratamento de erros
|
||
|
||
| Situação | Comportamento obrigatório |
|
||
| ---------------------------------------- | ----------------------------------------------------- |
|
||
| JSON inválido | Encerrar o processamento e não gerar arquivo de saída |
|
||
| Raiz diferente de objeto | Encerrar o processamento e não gerar arquivo de saída |
|
||
| `articles` ausente ou diferente de lista | Encerrar o processamento e não gerar arquivo de saída |
|
||
| `articles` vazio | Gerar arquivo com lista vazia e sem outras alterações |
|
||
| Estrutura de uma biblioteca ausente | Tratar seu candidato como indisponível |
|
||
| Campo de conteúdo com tipo inválido | Tratar seu candidato como indisponível |
|
||
| Todas as bibliotecas indisponíveis | Selecionar `newspaper4k` |
|
||
| Falha ao gravar o arquivo | Não deixar arquivo de saída parcialmente gravado |
|
||
|
||
Um erro em um artigo não pode impedir a seleção dos demais artigos, desde que o JSON e a lista `articles` sejam válidos.
|
||
|
||
## 10. Requisitos não funcionais
|
||
|
||
| ID | Requisito |
|
||
| ------- | ----------------------------------------------------------------------------------------- |
|
||
| NFR-001 | O processamento deve ser totalmente determinístico. |
|
||
| NFR-002 | O processamento não deve realizar chamadas de rede. |
|
||
| NFR-003 | O processamento não deve depender de LLM, embeddings ou serviços externos. |
|
||
| NFR-004 | O processamento deve operar somente sobre os dados do arquivo recebido. |
|
||
| NFR-005 | A gravação do arquivo deve ser atômica: sucesso completo ou ausência do arquivo de saída. |
|
||
|
||
## 11. Critérios de aceite
|
||
|
||
1. Dado o JSON de referência com 20 artigos, o arquivo de saída contém os mesmos 20 artigos na mesma ordem.
|
||
2. Cada artigo contém exatamente um `selected_extractor` válido.
|
||
3. Nenhum artigo contém `selected_extractor` nulo, vazio ou ambíguo.
|
||
4. Todas as chaves e valores anteriores permanecem semanticamente idênticos.
|
||
5. Nenhuma chave adicional, além de `selected_extractor`, é criada.
|
||
6. O arquivo original permanece inalterado.
|
||
7. Duas execuções sobre o mesmo arquivo produzem os mesmos valores de `selected_extractor`.
|
||
8. A seleção usa somente `trafilatura.text`, `newspaper4k.text` e `readability.cleaned_text`.
|
||
9. Quando todas as saídas estiverem vazias ou indisponíveis, `selected_extractor` recebe `newspaper4k`.
|
||
10. Quando não houver consenso, o desempate segue exatamente as regras da seção 7.6.
|
||
11. Quando houver empate técnico, o desempate segue exatamente as regras da seção 7.5.
|
||
12. Um JSON inválido ou sem `articles` válido não produz arquivo parcial.
|
||
|
||
## 12. Casos obrigatórios de teste
|
||
|
||
| Caso | Condição | Resultado esperado |
|
||
| ------ | ---------------------------------------------------------------------------- | ------------------------------------------------- |
|
||
| CT-001 | Três candidatos com consenso e um vencedor claro | Selecionar o maior `score` |
|
||
| CT-002 | Dois ou mais candidatos dentro de `0,03` do maior `score` | Selecionar o de menor quantidade de shingles |
|
||
| CT-003 | Empate técnico e mesma quantidade de shingles | Aplicar prioridade final |
|
||
| CT-004 | Três candidatos sem consenso | Selecionar a quantidade mediana de shingles |
|
||
| CT-005 | Dois candidatos sem consenso | Selecionar a maior quantidade de shingles |
|
||
| CT-006 | Somente um candidato utilizável | Selecionar esse candidato |
|
||
| CT-007 | Nenhum utilizável, mas existe candidato degradado | Executar o algoritmo somente com os degradados |
|
||
| CT-008 | Todos os candidatos indisponíveis | Selecionar `newspaper4k` |
|
||
| CT-009 | Readability retorna apenas um fragmento pequeno enquanto os outros concordam | O fragmento perde por baixa cobertura do consenso |
|
||
| CT-010 | Um candidato contém o conteúdo comum e muito conteúdo excedente | O candidato perde suporte e reduz sua pontuação |
|
||
| CT-011 | Um candidato contém somente parte do conteúdo comum | O candidato perde cobertura e reduz sua pontuação |
|
||
| CT-012 | A entrada já contém `selected_extractor` | Recalcular e substituir somente essa chave |
|
||
| CT-013 | `articles` está vazio | Gerar saída válida com `articles` vazio |
|
||
| CT-014 | JSON inválido | Não gerar saída |
|
||
|
||
## 13. Definition of Done
|
||
|
||
- Todos os requisitos funcionais foram implementados.
|
||
- Todos os casos obrigatórios de teste foram automatizados e aprovados.
|
||
- O JSON de referência com 20 artigos é processado integralmente.
|
||
- A saída contém somente a inclusão de `selected_extractor` em cada artigo.
|
||
- O arquivo original permanece inalterado.
|
||
- Execuções repetidas sobre a mesma entrada produzem as mesmas escolhas.
|
||
- Não existe caminho de execução que produza `ambiguous`, `null` ou artigo sem seleção.
|