feat: add deterministic content extractor selector engine with F1 consensus
This commit is contained in:
@@ -0,0 +1,137 @@
|
||||
# Data Model: Deterministic Content Selection
|
||||
|
||||
**Branch**: `004-deterministic-content-selection` | **Date**: 2026-08-20 | **Spec**: [spec.md](spec.md)
|
||||
|
||||
---
|
||||
|
||||
## 1. Domain Entities & Value Types
|
||||
|
||||
```mermaid
|
||||
classDiagram
|
||||
class ExtractorName {
|
||||
<<enumeration>>
|
||||
TRAFILATURA = "trafilatura"
|
||||
NEWSPAPER4K = "newspaper4k"
|
||||
READABILITY = "readability"
|
||||
}
|
||||
|
||||
class CandidateStatus {
|
||||
<<enumeration>>
|
||||
USABLE
|
||||
DEGRADED
|
||||
UNAVAILABLE
|
||||
}
|
||||
|
||||
class ExtractorCandidate {
|
||||
+ExtractorName name
|
||||
+str raw_text
|
||||
+str error
|
||||
+CandidateStatus status
|
||||
+List~str~ tokens
|
||||
+Set~Tuple~ shingles
|
||||
+int shingle_count
|
||||
+float coverage
|
||||
+float support
|
||||
+float score
|
||||
}
|
||||
|
||||
class ArticleSelectionResult {
|
||||
+int article_index
|
||||
+ExtractorName selected_extractor
|
||||
+str selection_reason
|
||||
+int active_candidates_count
|
||||
+int consensus_shingles_count
|
||||
+Dict~ExtractorName, ExtractorCandidate~ candidates
|
||||
}
|
||||
|
||||
class BatchProcessingResult {
|
||||
+int total_articles
|
||||
+int processed_count
|
||||
+Dict~str, int~ selection_distribution
|
||||
+str input_file
|
||||
+str output_file
|
||||
}
|
||||
|
||||
ExtractorCandidate --> ExtractorName
|
||||
ExtractorCandidate --> CandidateStatus
|
||||
ArticleSelectionResult --> ExtractorName
|
||||
ArticleSelectionResult --> ExtractorCandidate
|
||||
BatchProcessingResult --> ArticleSelectionResult
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. Entity Descriptions & Fields
|
||||
|
||||
### `ExtractorName` (Enum / Literal)
|
||||
Enumeração estrita com os três motores de extração suportados:
|
||||
- `"trafilatura"`
|
||||
- `"newspaper4k"`
|
||||
- `"readability"`
|
||||
|
||||
### `CandidateStatus` (Enum)
|
||||
Classificação do estado de cada extrator em um dado artigo:
|
||||
- `USABLE`: Campo de texto contém string não-vazia após normalização e campo `error` é nulo/vazio.
|
||||
- `DEGRADED`: Campo de texto contém string não-vazia após normalização, porém campo `error` não é nulo.
|
||||
- `UNAVAILABLE`: Campo de texto é ausente, nulo, tipo diferente de string ou vazio após normalização.
|
||||
|
||||
### `ExtractorCandidate` (Dataclass)
|
||||
Representação estruturada de um candidato durante o cálculo:
|
||||
|
||||
| Campo | Tipo | Descrição |
|
||||
|---|---|---|
|
||||
| `name` | `ExtractorName` | Identificador do motor de extração (`trafilatura`, `newspaper4k`, `readability`). |
|
||||
| `raw_text` | `str \| None` | Texto bruto obtido do campo correspondente no JSON (`trafilatura.text`, `newspaper4k.text`, `readability.cleaned_text`). |
|
||||
| `error` | `str \| None` | Mensagem de erro do motor, se houver (`trafilatura.error`, etc.). |
|
||||
| `status` | `CandidateStatus` | Estado de viabilidade do candidato (`USABLE`, `DEGRADED`, `UNAVAILABLE`). |
|
||||
| `tokens` | `list[str]` | Sequência ordenada de tokens alfanuméricos minúsculos após normalização NFKC. |
|
||||
| `shingles` | `set[tuple[str, ...]]` | Conjunto de n-grams consecutivos de 5 tokens (ou 1 n-gram se $1 \le \text{tokens} \le 4$). |
|
||||
| `shingle_count` | `int` | Quantidade total de shingles gerados (`len(shingles)`). |
|
||||
| `coverage` | `float` | Proporção de shingles do consenso presentes no candidato ($[0.0, 1.0]$). |
|
||||
| `support` | `float` | Proporção de shingles do candidato que pertencem ao consenso ($[0.0, 1.0]$). |
|
||||
| `score` | `float` | Pontuação $F_1$ baseada em cobertura e suporte ($[0.0, 1.0]$). |
|
||||
|
||||
---
|
||||
|
||||
### `ArticleSelectionResult` (Dataclass)
|
||||
Resultado detalhado da avaliação para um único artigo:
|
||||
|
||||
| Campo | Tipo | Descrição |
|
||||
|---|---|---|
|
||||
| `article_index` | `int` | Posição ordinal do artigo no array `articles` original (0-indexed). |
|
||||
| `selected_extractor` | `ExtractorName` | Vencedor da seleção determinística (`trafilatura`, `newspaper4k`, `readability`). |
|
||||
| `selection_reason` | `str` | Justificativa rastreável da escolha (ex: `"highest_score"`, `"technical_tie_smallest_shingles"`, `"no_consensus_median_shingles"`, `"single_usable_candidate"`, `"fallback_all_unavailable"`). |
|
||||
| `active_candidates_count` | `int` | Número de candidatos que formaram o conjunto ativo avaliado. |
|
||||
| `consensus_shingles_count` | `int` | Quantidade de shingles no conjunto de consenso. |
|
||||
| `candidates` | `dict[ExtractorName, ExtractorCandidate]` | Dicionário com o detalhamento de cada um dos 3 motores. |
|
||||
|
||||
---
|
||||
|
||||
### `BatchProcessingResult` (Dataclass)
|
||||
Sumário da execução do lote:
|
||||
|
||||
| Campo | Tipo | Descrição |
|
||||
|---|---|---|
|
||||
| `total_articles` | `int` | Total de artigos encontrados no arquivo de entrada. |
|
||||
| `processed_count` | `int` | Total de artigos processados e enriquecidos com sucesso. |
|
||||
| `selection_distribution` | `dict[str, int]` | Contagem de seleções por motor (`{"trafilatura": X, "newspaper4k": Y, "readability": Z}`). |
|
||||
| `input_file` | `str` | Caminho do arquivo lido. |
|
||||
| `output_file` | `str` | Caminho do arquivo gerado de forma atômica. |
|
||||
|
||||
---
|
||||
|
||||
## 3. JSON Schema Mapping
|
||||
|
||||
### Entrada
|
||||
- Raiz: Objeto contendo chave `articles: list[dict]`.
|
||||
- Cada item em `articles`:
|
||||
- `trafilatura` (objeto opcional): `{ "text": str | null, "error": str | null, ... }`
|
||||
- `newspaper4k` (objeto opcional): `{ "text": str | null, "error": str | null, ... }`
|
||||
- `readability` (objeto opcional): `{ "cleaned_text": str | null, "error": str | null, ... }`
|
||||
|
||||
### Saída
|
||||
- Mesma estrutura exata da entrada, preservando 100% dos dados anteriores e ordem da lista `articles`.
|
||||
- Em cada item de `articles`, adição/atualização da chave:
|
||||
```json
|
||||
"selected_extractor": "newspaper4k" | "readability" | "trafilatura"
|
||||
```
|
||||
Reference in New Issue
Block a user