Files
TextNLPClassifierApp/specs/007-media-article-routing/tasks.md
T

149 lines
16 KiB
Markdown

# Tasks: Classificação e Roteamento de Notícias Predominantemente de Mídia
**Feature**: `007-media-article-routing`
**Input**: [spec.md](spec.md), [plan.md](plan.md), [data-model.md](data-model.md), [research.md](research.md), [contracts/](contracts/)
---
## Phase 1: Foundational (Blocking Prerequisites)
**Purpose**: Estruturas de dados internas, constante do schema e helper de comunicação HTTP via `urllib.request` que bloqueiam a implementação das histórias.
- [x] T001 Implement `MediaCandidateInfo` dataclass and `MediaClassification` type definition in `scripts/extract_article_contents.py`
- [x] T002 Define `MEDIA_CLASSIFIER_SCHEMA` constant (flat 2-field schema matching `contracts/classifier-io.schema.json`) and implement `validate_classifier_response(data: dict) -> MediaClassification | None` in `scripts/extract_article_contents.py`
- [x] T003 Implement `urllib.request` JSON HTTP dispatch helper `_http_post_json(url: str, payload: dict, headers: dict, timeout: int) -> tuple[int, str]` in `scripts/extract_article_contents.py`
- Serializar request JSON, configurar headers e executar chamada via `urllib.request`;
- Retornar tupla `(http_status, response_body)`;
- Não converter erros de transporte para status mágicos (ex: 0/-1); exceções operacionais (`URLError`, `HTTPError`, timeout) devem propagar para captura e tratamento centralizado de fallback em `classify_media_content()`.
**Checkpoint**: Base foundational pronta — implementação das histórias de usuário pode prosseguir.
---
## Phase 2: User Story 1 - Detecção e Roteamento de Publicações Predominantemente de Mídia (Priority: P1)
**Goal**: Identificar publicações cujo conteúdo informativo principal seja mídia (vídeo, imagem, imagens, embed ou mídia mista), desviar antes dos extratores textuais e persistir em `*_media.json` com envelope mínimo `{ "articles": [...] }`.
**Independent Test**: Submeter amostras de páginas de mídia com texto introdutório curto (cenários B, C, D, E, F); verificar que `extract_all_engines` NÃO é chamado e que o arquivo `*_media.json` é gerado contendo o envelope e os registros `MediaArticle`.
- [x] T004 [US1] Unit tests for schema validation, DOM structural gate (`detect_candidate_media`) and compact payload generation (`build_compact_payload`) in `tests/unit/test_media_classifier.py`
- Validar `validate_classifier_response`: `content_type=text` + `media_type=null` (válido), `content_type=text` + `media_type=image` (inválido), `content_type=media` + `media_type=video` (válido), `content_type=media` + `media_type=null` (inválido), valores desconhecidos (inválido), campo obrigatório ausente (inválido), campo extra (inválido);
- Validar detecção de `<video>`;
- Validar que `<source>` isolado NÃO é vídeo;
- Validar que `<figure><img></figure>` e `<picture><img></picture>` contam exatamente 1 imagem;
- Validar contagem exata de múltiplos `<img>`;
- Validar detecção de `<iframe>`, `<embed>`, `<object>` como embeds;
- Validar que mídia em `<header>`, `<nav>`, `<footer>` e `<aside>` não aciona o gate;
- Validar ordem estrutural (`<article>` $\rightarrow$ `<main>` $\rightarrow$ `[role="main"]` $\rightarrow$ `<body>`);
- Validar que `build_compact_payload` extrai `title`, `text_content` (todos os `<p>` editoriais normalizados sem truncamento arbitrário e preservando Unicode) e `media_summary` (`has_video`, `image_count`, `has_embed`).
- [x] T005 [US1] Implement `detect_candidate_media(soup: BeautifulSoup) -> MediaCandidateInfo` identifying editorial region (`<article>`, `<main>`, `[role=main]`, `<body>`) and media elements (`<video>`, real `<img>` count without duplicate wrappers, `<iframe>`/`<embed>`/`<object>`) in `scripts/extract_article_contents.py`
- [x] T006 [US1] Implement `build_compact_payload(soup: BeautifulSoup, candidate_info: MediaCandidateInfo) -> str` extracting title and normalized editorial text in `scripts/extract_article_contents.py`
- [x] T007 [US1] Implement single prompt constant and initial `classify_media_content(payload: str, metrics: dict, silent: bool = False) -> tuple[MediaClassification | None, str | None]` in `scripts/extract_article_contents.py`
- Definir constante de prompt único `MEDIA_CLASSIFIER_PROMPT` em `scripts/extract_article_contents.py` reutilizada por Ollama, Groq e OmniRoute, comum a todos os idiomas, solicitando exclusivamente `content_type` e `media_type`, contendo definições semânticas de text vs media, e sem solicitar reasoning, confidence, rationale, summary, keywords, evidence ou tradução;
- Despachar provedor primário Ollama (`OLLAMA_ENDPOINT`, `OLLAMA_MODEL="qwen3.5:2b"`, `OLLAMA_TIMEOUT=10`, `think=false`, `temperature=0.0`, `format=MEDIA_CLASSIFIER_SCHEMA`) e validar schema da resposta;
- Registrar no stderr o provedor utilizado e a classificação final (`text` ou `media` + `media_type`) respeitando `silent`, sem logar o payload textual integral.
- [x] T008 [US1] Implement media output path resolution, metrics dict initialization, `save_media_json`, and batch routing in `scripts/extract_article_contents.py`
- Inicializar em `process_batch` o dicionário local com as 11 métricas operacionais zeradas;
- Implementar resolução do caminho do arquivo de mídia: sem `-o` $\rightarrow$ `<input_dir>/<input_stem>_media.json` (ex: `out/river_plate.json` $\rightarrow$ `out/river_plate_media.json`); com `-o` $\rightarrow$ `<output_dir>/<output_stem>_media.json` (ex: `-o out/processados/resultado.json` $\rightarrow$ `out/processados/resultado_media.json`) sem novas flags CLI;
- Implementar `save_media_json(articles: list[dict[str, Any]], output_path: Path) -> None` gravando `{ "articles": [...] }`;
- Integrar o fluxo em `process_batch`: após crawl bem-sucedido, carregar HTML no BeautifulSoup e executar `detect_candidate_media()`; se `has_candidate_media == True`, executar `build_compact_payload()` e `classify_media_content(payload, metrics, silent)`; se retornar `content_type == "media"`, adicionar `MediaArticle` em `media_articles` e NÃO executar `extract_all_engines()`.
- [x] T009 [US1] Integration tests for media routing (Cenários B, C, D, E, F), `_media.json` naming resolution, and `input_meta` preservation with custom unknown fields in `tests/integration/test_media_routing.py`
- Validar que campos desconhecidos arbitrários (ex: `custom_field="preserve-me"`, `custom_number=42`) são integralmente preservados em `MediaArticle`.
**Checkpoint**: User Story 1 completa e testável de forma independente com Ollama mockado.
---
## Phase 3: User Story 2 - Roteamento Direto e Preservação de Notícias Textuais (Priority: P2)
**Goal**: Garantir que artigos textuais substantivos (com ou sem mídias ilustrativas) ou páginas sem mídia candidata continuem sendo processados pelos 3 motores textuais em `*_extracted.json`, gerando incondicionalmente `*_media.json` com `{"articles": []}` caso não haja mídias.
**Independent Test**: Submeter página sem mídia candidata (Cenário A) e matérias jornalísticas longas com fotos/vídeos (Cenários G, H); verificar execução dos 3 motores, gravação no JSON textual e presença de `*_media.json` com `{"articles": []}`.
- [x] T010 [US2] Unit tests for direct gate bypass (`has_candidate_media == False`) and textual classification handling in `tests/unit/test_media_classifier.py`
- [x] T011 [US2] Implement direct gate bypass in `process_batch` when `has_candidate_media == False` routing directly to `extract_all_engines` without LLM calls, ensure `content_type == "text"` passes to `extract_all_engines`, and execute `save_media_json` ensuring `_media.json` is always generated (with `{"articles": []}` when zero media articles) in `scripts/extract_article_contents.py`
- [x] T012 [US2] Integration tests for textual articles (Cenários A, G, H), main JSON report counters (`total_articles`, `successful_articles`, `failed_articles`), mandatory generation of `_media.json` with `{"articles": []}`, and `input_meta` preservation in `tests/integration/test_media_routing.py`
- Validar que os mesmos campos desconhecidos arbitrários em `input_meta` sobrevivem integralmente nos registros de artigos textuais em `*_extracted.json`.
**Checkpoint**: User Stories 1 e 2 funcionais e integradas, com preservação estrita do pipeline textual e arquivo de mídia incondicional.
---
## Phase 4: User Story 3 - Resiliência com Fallback Operacional Sequencial e Registro de Falhas (Priority: P3)
**Goal**: Implementar a cadeia sequencial de contingência (Ollama $\rightarrow$ Groq $\rightarrow$ OmniRoute), registrando falhas operacionais cumulativas inline no JSON principal com `classification_status: "failed"` sem abortar o lote e mantendo suporte multilíngue.
**Independent Test**: Simular os 5 estados de provedores (Cenários I, J, K, L, M) via mocks offline; verificar transição Groq/OmniRoute, first-valid-wins, registro de erro inline com `classification_status: "failed"` e continuidade do lote.
- [x] T013 [US3] Unit tests for sequential provider chain in `tests/unit/test_media_classifier.py`
- Validar first-valid-wins (Ollama sucesso $\rightarrow$ Groq e OmniRoute com 0 chamadas; Ollama falha + Groq sucesso $\rightarrow$ OmniRoute com 0 chamadas);
- Validar transição imediata para o próximo provider em caso de timeout, connection error, HTTP error ou schema inválido;
- Validar que providers recebem parâmetros corretos (Ollama: `think: false`, `temperature: 0.0`; Groq: `openai/gpt-oss-20b`, `reasoning_effort: "low"`, `temperature: 0.0`, strict JSON Schema; OmniRoute: `cgpt-web/gpt-5.5`, `temperature: 0.0`, JSON Schema);
- Validar que os 3 providers utilizam o mesmo prompt único, sem variantes por idioma e sem solicitações de campos proibidos (reasoning, confidence, rationale, summary, keywords, evidence, tradução);
- Validar ausência de retries, votação, juiz ou confidence thresholds.
- [x] T014 [US3] Extend `classify_media_content(payload, metrics, silent)` with Groq (`GROQ_ENDPOINT`, `GROQ_API_KEY`, `GROQ_MODEL="openai/gpt-oss-20b"`, `GROQ_TIMEOUT=15`, `reasoning_effort="low"`, `temperature=0.0`, strict JSON Schema) and OmniRoute (`OMNIROUTE_ENDPOINT`, `OMNIROUTE_API_KEY`, `OMNIROUTE_MODEL="cgpt-web/gpt-5.5"`, `OMNIROUTE_TIMEOUT=20`, `temperature=0.0`, JSON Schema), incrementing `fallback_groq` e `fallback_omniroute` imediatamente antes de cada chamada, registrando logs operacionais de fallback no stderr respeitando `silent`, e tratando configuração ausente como falha operacional do provider in `scripts/extract_article_contents.py`
- [x] T015 [US3] Implement cumulative failure handling in `process_batch` recording inline in main JSON with `classification_status = "failed"`, `error_message`, incrementing `classification_failed`, registrando log de falha total no stderr respeitando `silent`, preservando crawl metadata, skipping multimotor, não adicionando o registro de falha ao `media_articles` nem ao array `articles` de `*_media.json`, e incrementing `failed_articles` in `scripts/extract_article_contents.py`
- [x] T016 [US3] Integration tests for cumulative provider failure (Cenário K), fallback transitions (Cenários I, J, L), `input_meta` preservation in failure records, and multilingual support (Cenário M) across supported pipeline languages without language-specific prompts in `tests/integration/test_media_routing.py`
- Validar no Cenário K que `classification_status == "failed"`, `error_message` existe, `extract_all_engines` NÃO é chamado, o registro NÃO entra em `*_media.json`, `failed_articles` incrementa exatamente uma vez, `input_meta` integral é preservado com campos desconhecidos, `crawled_url`, `page_title` e `http_status` disponíveis são preservados, e o lote continua normalmente para o próximo artigo.
**Checkpoint**: As 3 User Stories estão completas, com resiliência total, fallback sequencial determinístico e tratamento de falhas.
---
## Phase 5: Polish & Observabilidade
**Purpose**: Métricas operacionais, segurança de credenciais, compatibilidade CLI e validação estática de Zero-Regex.
- [x] T017 [Polish] Implement in-memory tracking of remaining operational metrics (`total_evaluated`, `text`, `media`, and `media/<subtype>`) and output summary to stderr respecting `--silent` in `scripts/extract_article_contents.py`
- Garantir que as métricas `total_evaluated`, `text`, `media`, `media/video`, `media/image`, `media/images`, `media/embed`, `media/mixed`, `fallback_groq`, `fallback_omniroute` e `classification_failed` sejam incrementadas uma única vez nos pontos definidos, sem duplicidade;
- Ao final do lote, emitir o summary formatado no stderr respeitando `--silent`.
- [x] T018 [P] Verify and update `tests/scripts/check_zero_regex.py` to include `scripts/extract_article_contents.py` and new test files in the scoped AST check
- [x] T019 Integration tests verifying the 11 metrics values, logging in stderr, `--silent` suppression, secret non-exposure, CLI flag compatibility, and exit code preservation in `tests/integration/test_media_routing.py`
- Validar contagem exata das 11 métricas nos pontos definidos;
- Validar que o provider utilizado, acionamentos de fallback (Ollama $\rightarrow$ Groq, Groq $\rightarrow$ OmniRoute), classificação final e eventuais falhas totais são emitidos no stderr quando não silencioso;
- Validar que `--silent` suprime todos os logs operacionais e o resumo de métricas;
- Validar que API keys, tokens e secrets NUNCA aparecem em stderr, JSON de saída ou `error_message`;
- Validar que o payload textual integral enviado ao classificador NÃO aparece no log/stderr por padrão (sem proibir o texto normal pertencente ao contrato de `*_extracted.json`);
- Validar compatibilidade dos argumentos CLI (`-i`, `-o`, `-l`, `--lang`, `-t`, `-s`), resolução dos outputs e preservação dos exit codes existentes (`0`, `1`, `2`), sem criar novos testes complexos de SIGINT/Ctrl+C/exit 130 exclusivamente para esta feature.
- [x] T020 Execute validation commands from `specs/007-media-article-routing/quickstart.md` (`pytest` suite and `python tests/scripts/check_zero_regex.py`)
---
## Dependencies & Execution Order
```mermaid
flowchart TD
Foundational[Phase 1: Foundational T001-T003] --> US1[Phase 2: User Story 1 - P1 T004-T009]
US1 --> US2[Phase 3: User Story 2 - P2 T010-T012]
US2 --> US3[Phase 4: User Story 3 - P3 T013-T016]
US3 --> Polish[Phase 5: Polish & Observability T017-T020]
Polish17[T017] --> Polish19[T019]
Polish18[T018] --> Polish20[T020]
Polish19 --> Polish20
```
### Phase Dependencies
- **Foundational (Phase 1)**: Sem dependências de histórias; implementa tipos, schema e cliente HTTP básico.
- **User Story 1 (Phase 2 - P1)**: Depende de Foundational. Implementa detecção DOM, payload, Ollama e persistência `*_media.json`.
- **User Story 2 (Phase 3 - P2)**: Depende de US1. Implementa bypass direto sem LLM, preservação do pipeline textual e emissão incondicional de `*_media.json` com `[]`.
- **User Story 3 (Phase 4 - P3)**: Depende de US2. Implementa fallbacks Groq/OmniRoute na mesma função `classify_media_content`, tratamento de falhas inline e multilíngue.
- **Polish (Phase 5)**: Depende de US1, US2 e US3. `T017` implementa o fechamento das métricas, `T019` testa métricas, segurança e CLI, `T018` atualiza o checker AST de forma independente, e `T020` executa a validação global final.
---
## Parallel Execution Opportunities
- **Phase 5 (Polish)**: `T018` (atualização do checker AST de zero-regex em `tests/scripts/check_zero_regex.py`) está marcado com `[P]` pois opera em arquivo independente e pode rodar em paralelo às demais tarefas.
---
## Implementation Strategy
A entrega da feature será realizada de forma incremental e ordenada:
1. **Fundação**: Dataclasses, constante do schema e helper HTTP `_http_post_json`.
2. **História 1**: Detecção DOM, compact payload, Ollama, inicialização das 11 métricas e geração de `*_media.json`.
3. **História 2**: Gate bypass sem LLM, preservação textual e geração incondicional de `*_media.json` (com `[]` quando vazio).
4. **História 3**: Extensão da cadeia sequencial com Groq (`reasoning_effort="low"`) e OmniRoute, com registro inline de falha no JSON principal.
5. **Polimento**: Fechamento da contagem e resumo das 11 métricas no stderr, verificação de não-exposição de segredos, compatibilidade CLI e verificação estática Zero-Regex.
6. **Conclusão**: 100% das 20 tarefas concluídas e validadas contra a suíte de testes e o checker Zero-Regex.