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
MediaCandidateInfodataclass andMediaClassificationtype definition inscripts/extract_article_contents.py - T002 Define
MEDIA_CLASSIFIER_SCHEMAconstant (flat 2-field schema matchingcontracts/classifier-io.schema.json) and implementvalidate_classifier_response(data: dict) -> MediaClassification | Noneinscripts/extract_article_contents.py - T003 Implement
urllib.requestJSON HTTP dispatch helper_http_post_json(url: str, payload: dict, headers: dict, timeout: int) -> tuple[int, str]inscripts/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 emclassify_media_content().
- Serializar request JSON, configurar headers e executar chamada via
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) intests/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_payloadextraititle,text_content(todos os<p>editoriais normalizados sem truncamento arbitrário e preservando Unicode) emedia_summary(has_video,image_count,has_embed).
- Validar
- T005 [US1] Implement
detect_candidate_media(soup: BeautifulSoup) -> MediaCandidateInfoidentifying editorial region (<article>,<main>,[role=main],<body>) and media elements (<video>, real<img>count without duplicate wrappers,<iframe>/<embed>/<object>) inscripts/extract_article_contents.py - T006 [US1] Implement
build_compact_payload(soup: BeautifulSoup, candidate_info: MediaCandidateInfo) -> strextracting title and normalized editorial text inscripts/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]inscripts/extract_article_contents.py- Definir constante de prompt único
MEDIA_CLASSIFIER_PROMPTemscripts/extract_article_contents.pyreutilizada por Ollama, Groq e OmniRoute, comum a todos os idiomas, solicitando exclusivamentecontent_typeemedia_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 (
textoumedia+media_type) respeitandosilent, sem logar o payload textual integral.
- Definir constante de prompt único
- T008 [US1] Implement media output path resolution, metrics dict initialization,
save_media_json, and batch routing inscripts/extract_article_contents.py- Inicializar em
process_batcho 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\rightarrowout/river_plate_media.json); com-o\rightarrow<output_dir>/<output_stem>_media.json(ex:-o out/processados/resultado.json\rightarrowout/processados/resultado_media.json) sem novas flags CLI; - Implementar
save_media_json(articles: list[dict[str, Any]], output_path: Path) -> Nonegravando{ "articles": [...] }; - Integrar o fluxo em
process_batch: após crawl bem-sucedido, carregar HTML no BeautifulSoup e executardetect_candidate_media(); sehas_candidate_media == True, executarbuild_compact_payload()eclassify_media_content(payload, metrics, silent); se retornarcontent_type == "media", adicionarMediaArticleemmedia_articlese NÃO executarextract_all_engines().
- Inicializar em
- T009 [US1] Integration tests for media routing (Cenários B, C, D, E, F),
_media.jsonnaming resolution, andinput_metapreservation with custom unknown fields intests/integration/test_media_routing.py- Validar que campos desconhecidos arbitrários (ex:
custom_field="preserve-me",custom_number=42) são integralmente preservados emMediaArticle.
- Validar que campos desconhecidos arbitrários (ex:
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 intests/unit/test_media_classifier.py - T011 [US2] Implement direct gate bypass in
process_batchwhenhas_candidate_media == Falserouting directly toextract_all_engineswithout LLM calls, ensurecontent_type == "text"passes toextract_all_engines, and executesave_media_jsonensuring_media.jsonis always generated (with{"articles": []}when zero media articles) inscripts/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.jsonwith{"articles": []}, andinput_metapreservation intests/integration/test_media_routing.py- Validar que os mesmos campos desconhecidos arbitrários em
input_metasobrevivem integralmente nos registros de artigos textuais em*_extracted.json.
- Validar que os mesmos campos desconhecidos arbitrários em
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
\rightarrowGroq e OmniRoute com 0 chamadas; Ollama falha + Groq sucesso\rightarrowOmniRoute 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.
- Validar first-valid-wins (Ollama sucesso
- 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), incrementingfallback_groqefallback_omnirouteimediatamente antes de cada chamada, registrando logs operacionais de fallback no stderr respeitandosilent, e tratando configuração ausente como falha operacional do provider inscripts/extract_article_contents.py - T015 [US3] Implement cumulative failure handling in
process_batchrecording inline in main JSON withclassification_status = "failed",error_message, incrementingclassification_failed, registrando log de falha total no stderr respeitandosilent, preservando crawl metadata, skipping multimotor, não adicionando o registro de falha aomedia_articlesnem ao arrayarticlesde*_media.json, e incrementingfailed_articlesinscripts/extract_article_contents.py - T016 [US3] Integration tests for cumulative provider failure (Cenário K), fallback transitions (Cenários I, J, L),
input_metapreservation in failure records, and multilingual support (Cenário M) across supported pipeline languages without language-specific prompts intests/integration/test_media_routing.py- Validar no Cenário K que
classification_status == "failed",error_messageexiste,extract_all_enginesNÃO é chamado, o registro NÃO entra em*_media.json,failed_articlesincrementa exatamente uma vez,input_metaintegral é preservado com campos desconhecidos,crawled_url,page_titleehttp_statusdisponíveis são preservados, e o lote continua normalmente para o próximo artigo.
- Validar no Cenário K que
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, andmedia/<subtype>) and output summary to stderr respecting--silentinscripts/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_omnirouteeclassification_failedsejam incrementadas uma única vez nos pontos definidos, sem duplicidade; - Ao final do lote, emitir o summary formatado no stderr respeitando
--silent.
- Garantir que as métricas
- T018 [P] Verify and update
tests/scripts/check_zero_regex.pyto includescripts/extract_article_contents.pyand new test files in the scoped AST check - T019 Integration tests verifying the 11 metrics values, logging in stderr,
--silentsuppression, secret non-exposure, CLI flag compatibility, and exit code preservation intests/integration/test_media_routing.py- Validar contagem exata das 11 métricas nos pontos definidos;
- Validar que o provider utilizado, acionamentos de fallback (Ollama
\rightarrowGroq, Groq\rightarrowOmniRoute), classificação final e eventuais falhas totais são emitidos no stderr quando não silencioso; - Validar que
--silentsuprime 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(pytestsuite andpython 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.jsoncom[]. - 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.
T017implementa o fechamento das métricas,T019testa métricas, segurança e CLI,T018atualiza o checker AST de forma independente, eT020executa a validação global final.
Parallel Execution Opportunities
- Phase 5 (Polish):
T018(atualização do checker AST de zero-regex emtests/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:
- Fundação: Dataclasses, constante do schema e helper HTTP
_http_post_json. - História 1: Detecção DOM, compact payload, Ollama, inicialização das 11 métricas e geração de
*_media.json. - História 2: Gate bypass sem LLM, preservação textual e geração incondicional de
*_media.json(com[]quando vazio). - História 3: Extensão da cadeia sequencial com Groq (
reasoning_effort="low") e OmniRoute, com registro inline de falha no JSON principal. - 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.
- Conclusão: 100% das 20 tarefas concluídas e validadas contra a suíte de testes e o checker Zero-Regex.