Files

16 KiB

Tasks: Classificação e Roteamento de Notícias Predominantemente de Mídia

Feature: 007-media-article-routing
Input: spec.md, plan.md, data-model.md, research.md, 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.

  • T001 Implement MediaCandidateInfo dataclass and MediaClassification type definition in scripts/extract_article_contents.py
  • 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
  • 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.

  • 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).
  • 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
  • T006 [US1] Implement build_compact_payload(soup: BeautifulSoup, candidate_info: MediaCandidateInfo) -> str extracting title and normalized editorial text in scripts/extract_article_contents.py
  • 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.
  • 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().
  • 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": []}.

  • T010 [US2] Unit tests for direct gate bypass (has_candidate_media == False) and textual classification handling in tests/unit/test_media_classifier.py
  • 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
  • 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.

  • 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.
  • 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
  • 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
  • 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.

  • 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.
  • 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
  • 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.
  • 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

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.