# 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 { <> TRAFILATURA = "trafilatura" NEWSPAPER4K = "newspaper4k" READABILITY = "readability" } class CandidateStatus { <> 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" ```