Compare commits

...
10 Commits
131 changed files with 84233 additions and 3373 deletions
+3
View File
@@ -0,0 +1,3 @@
OPENAI_API_KEY=
OPENAI_BASE_URL="https://omniroute.app.andreferraro.com/v1"
OPENAI_MODEL="cgpt-web/gpt-5.5"
+316 -74
View File
@@ -6,7 +6,7 @@
[![Type Checked](https://img.shields.io/badge/Type%20Check-Mypy-blue.svg)](https://mypy-lang.org/) [![Type Checked](https://img.shields.io/badge/Type%20Check-Mypy-blue.svg)](https://mypy-lang.org/)
[![Tests](https://img.shields.io/badge/Tests-Pytest%20(100%25%20Passing)-brightgreen.svg)](tests/) [![Tests](https://img.shields.io/badge/Tests-Pytest%20(100%25%20Passing)-brightgreen.svg)](tests/)
> Plataforma modular em Python para **Classificação Multilíngue de Inerência de Entidades (NLP/LLM/ECP)** e **Extração Inteligente de Manchetes de Notícias com Evasão Anti-Bot (Google News RSS & Foxcape)**. > Plataforma modular em Python para **Classificação Multilíngue de Inerência de Entidades (NLP/LLM/ECP)**, **Extração Inteligente de Manchetes (Google News RSS & Foxcape)**, **Extração Multimotor de Artigos (Trafilatura, Newspaper4k, Readability)** e **Seleção Determinística de Conteúdo por Consenso Textual ($F_1$ Shingles)**.
--- ---
@@ -29,6 +29,19 @@
- [Visão Geral e Tríplice Extração](#visão-geral-e-tríplice-extração) - [Visão Geral e Tríplice Extração](#visão-geral-e-tríplice-extração)
- [Argumentos e Flags CLI](#argumentos-e-flags-cli) - [Argumentos e Flags CLI](#argumentos-e-flags-cli)
- [Exemplos de Uso](#exemplos-de-uso) - [Exemplos de Uso](#exemplos-de-uso)
- [4. Seletor Determinístico de Conteúdo de Artigos](#4--seletor-determinístico-de-conteúdo-de-artigos)
- [Visão Geral e Algoritmo de Consenso ($F_1$)](#visão-geral-e-algoritmo-de-consenso-f_1)
- [Pipeline de Normalização e Shingles](#pipeline-de-normalização-e-shingles)
- [Critérios de Desempate Técnico e Resiliência](#critérios-de-desempate-técnico-e-resiliência)
- [Argumentos e Flags CLI](#argumentos-e-flags-cli-1)
- [Exemplos Práticos de Uso](#exemplos-práticos-de-uso-1)
- [5. Conversor de Artigo JSON para Markdown](#5--conversor-de-artigo-json-para-markdown)
- [Visão Geral e Estrutura do Documento](#visão-geral-e-estrutura-do-documento)
- [Isolamento Estrito de Extratores e Fallback](#isolamento-estrito-de-extratores-e-fallback)
- [Matriz Determinística de Metadados](#matriz-determinística-de-metadados)
- [Sanitização Editorial e Deduplicação](#sanitização-editorial-e-deduplicação)
- [Argumentos e Flags CLI](#argumentos-e-flags-cli-2)
- [Exemplos Práticos de Uso](#exemplos-práticos-de-uso-2)
- [Estrutura do Projeto](#-estrutura-do-projeto) - [Estrutura do Projeto](#-estrutura-do-projeto)
- [Testes e Qualidade de Código](#-testes-e-qualidade-de-código) - [Testes e Qualidade de Código](#-testes-e-qualidade-de-código)
- [Licença](#-licença) - [Licença](#-licença)
@@ -37,11 +50,13 @@
## 🌟 Visão Geral ## 🌟 Visão Geral
O **TextNLPClassifierApp** reúne ferramentas de engenharia de dados e processamento de linguagem natural: O **TextNLPClassifierApp** reúne um ecossistema completo de ferramentas de engenharia de dados e processamento de linguagem natural:
1. **`classify.py`**: Motor de classificação semântica e contextual que determina o grau de aderência e inerência de um documento Markdown em relação a uma entidade alvo definida em um **ECP Snapshot (Entity Context Profile)**. 1. **`classify.py`**: Motor de classificação semântica e contextual que determina o grau de aderência e inerência de um documento Markdown em relação a uma entidade alvo descrita em um **ECP Snapshot (Entity Context Profile)**.
2. **`scripts/extract_google_news.py`**: Extrator de notícias por palavra-chave, idioma e região geográfica que utiliza o motor stealth **Foxcape** (em modo headless), decodificação paralela de URLs para os links reais dos portais de notícias e feedback em tempo real. 2. **`scripts/extract_google_news.py`**: Extrator de notícias por palavra-chave, idioma e região geográfica utilizando navegação stealth **Foxcape** (headless), decodificação paralela de URLs para links reais e feedback em tempo real.
3. **`scripts/extract_article_contents.py`**: Extrator e parser de artigos multimotor com navegação stealth Foxcape headless e extração combinada via **Trafilatura**, **Newspaper4k** (NLP) e **Readability**, consolidando texto higienizado, autores, datas, imagens e resumos em JSON estruturado. 3. **`scripts/extract_article_contents.py`**: Extrator e parser de artigos multimotor com navegação stealth Foxcape headless e extração simultânea via **Trafilatura**, **Newspaper4k** (NLP) e **Readability**, consolidando texto higienizado, autores, datas, imagens e resumos.
4. **`scripts/select_article_extractor.py`**: Motor determinístico de seleção de extratores que avalia as saídas dos três motores, aplica normalização em memória, calcula métricas de consenso de shingles (5-tokens) com pontuação $F_1$, desempata tecnicamente ($\le 0.03$) favorecendo menor concisão/ruído e enriquece os dados de forma não-destrutiva e atômica.
5. **`scripts/convert_article_to_markdown.py`**: Conversor determinístico que recebe o JSON de um único artigo selecionado, isola estritamente o corpo do extrator vencedor (`trafilatura`, `newspaper4k` ou `readability`), resolve metadados editoriais por prioridade estrita, higieniza links/imagens/cabeçalhos e gera um documento Markdown (`.md`) padronizado com gravação atômica transacional.
--- ---
@@ -103,7 +118,7 @@ flowchart TD
* **Tier 1 (Determinístico / NLP Leve)**: Análise de frequência de termos, detecção de âncoras temáticas no primeiro terço do documento, contagem de aliases e penalização por âncoras negativas. * **Tier 1 (Determinístico / NLP Leve)**: Análise de frequência de termos, detecção de âncoras temáticas no primeiro terço do documento, contagem de aliases e penalização por âncoras negativas.
* **Tier 2 (Vetorial / Embeddings)** *(Opcional: `--enable-embeddings`)*: Projeção vetorial e cálculo de cosseno entre o perfil da entidade e os parágrafos do documento. * **Tier 2 (Vetorial / Embeddings)** *(Opcional: `--enable-embeddings`)*: Projeção vetorial e cálculo de cosseno entre o perfil da entidade e os parágrafos do documento.
* **Tier 3 (LLM Fallback)** *(Opcional: `--enable-llm`)*: Consulta a modelo de linguagem para desambiguação de casos limiares e sutilezas semânticas. * **Tier 3 (LLM Fallback)** *(Opcional: `--enable-llm`)*: Adaptador de desambiguação inteligente (`src/adapters/llm.py`) acionado exclusivamente para casos limiares e ambíguos (ex: menção isolada `TANGENTIAL` ou confiança `< 0.60`). Casos claros não chamam o LLM para economizar custos e latência; em caso de falha de conexão com a API, degrada graciosamente mantendo o resultado do Tier 1 com aviso registrado em `warnings`.
### Categorias de Decisão ### Categorias de Decisão
@@ -116,36 +131,53 @@ flowchart TD
### Formato do ECP Snapshot e Markdown ### Formato do ECP Snapshot e Markdown
#### Exemplo de ECP Snapshot (`ecp_sample.json`): #### Exemplo de ECP Snapshot Real (`examples/ecp_club_atletico_river_plate.json`):
```json ```json
{ {
"target_entity_id": "ent_river_plate", "target_entity_id": "ecp_river_plate",
"target_name": "River Plate", "target_name": "Club Atlético River Plate",
"aliases": ["Club Atlético River Plate", "Millonario", "El Más Grande"], "canonical_name": "Club Atlético River Plate",
"domain": "sports/football", "domain": "Futebol / Esportes",
"anchors": ["Monumental", "Copa Libertadores", "Marcelo Gallardo", "Copa Sudamericana"], "aliases": ["Club Atlético River Plate", "River Plate", "River", "El Millonario", "La Banda", "CARP"],
"negative_anchors": ["Boca Juniors vitória", "Flamengo campeão"], "anchors": ["Monumental", "Copa Libertadores", "Copa Sudamericana", "Eduardo Coudet", "Nicolás Otamendi"],
"negative_anchors": ["River Plate de Montevideo", "River Plate de Asunción", "Rio da Prata"],
"graph_version": "1.0.0", "graph_version": "1.0.0",
"related_entities": [ "related_entities": [
{ {
"entity_id": "ent_gallardo", "entity_id": "boca_juniors",
"name": "Marcelo Gallardo", "name": "Club Atlético Boca Juniors",
"relation_type": "manager", "relation_type": "RIVAL_OF",
"weight": 0.85, "weight": 0.9,
"aliases": ["Muñeco"] "aliases": ["Boca Juniors", "Boca", "Xeneize"]
},
{
"entity_id": "estadio_monumental",
"name": "Estadio Mâs Monumental",
"relation_type": "HOME_VENUE_OF",
"weight": 0.95,
"aliases": ["Monumental", "El Monumental"]
} }
] ]
} }
``` ```
Perfis de exemplo prontos para uso em `examples/`:
- [`examples/ecp_club_atletico_river_plate.json`](examples/ecp_club_atletico_river_plate.json) (Club Atlético River Plate)
- [`examples/ecp_sao_paulo_futebol_clube.json`](examples/ecp_sao_paulo_futebol_clube.json) (São Paulo FC)
### Exemplos de Uso CLI ### Exemplos de Uso CLI
```bash ```bash
# Classificação Determinística padrão (Tier 1) # Classificação Determinística padrão (Tier 1)
python classify.py --ecp tests/fixtures/sample_ecp.json --content tests/fixtures/sample_article.md python classify.py \
--ecp examples/ecp_club_atletico_river_plate.json \
--content out/markdown/meu_artigo_001.md
# Salvar resultado em arquivo JSON formatado # Salvar resultado em arquivo JSON formatado
python classify.py --ecp ecp.json --content artigo.md --output out/resultado_classificacao.json python classify.py \
--ecp examples/ecp_club_atletico_river_plate.json \
--content out/markdown/meu_artigo_001.md \
--output out/classification_001.json
# Habilitar camadas adicionais (Embeddings e LLM) # Habilitar camadas adicionais (Embeddings e LLM)
python classify.py --ecp ecp.json --content artigo.md --enable-embeddings --enable-llm python classify.py --ecp ecp.json --content artigo.md --enable-embeddings --enable-llm
@@ -157,7 +189,7 @@ python classify.py --ecp ecp.json --content artigo.md --enable-embeddings --enab
### O que é e Como Funciona ### O que é e Como Funciona
O script [`scripts/extract_google_news.py`](file:///c:/Users/aferr/Projects/AFTech/DunaMedia/TextNLPClassifierApp/scripts/extract_google_news.py) é um extrator CLI autônomo projetado para consultar o feed RSS do Google News com máxima velocidade, resiliência e integridade de dados. O script [`scripts/extract_google_news.py`](scripts/extract_google_news.py) é um extrator CLI autônomo projetado para consultar o feed RSS do Google News com máxima velocidade, resiliência e integridade de dados.
### Diferenciais Técnicos ### Diferenciais Técnicos
@@ -182,42 +214,24 @@ O script [`scripts/extract_google_news.py`](file:///c:/Users/aferr/Projects/AFTe
### Exemplos Práticos de Uso ### Exemplos Práticos de Uso
#### 1. River Plate (Argentina / Espanhol / 2 Páginas / Salvar em Arquivo)
```bash ```bash
# River Plate (Argentina / Espanhol / 2 Páginas / Salvar em Arquivo)
python scripts/extract_google_news.py -q "River Plate" -l es --locale AR -p 2 -o out/river_plate.json python scripts/extract_google_news.py -q "River Plate" -l es --locale AR -p 2 -o out/river_plate.json
```
* **Saída no Terminal**:
```text
[INFO] 🔍 Consultando Google News: 'River Plate' (idioma: es, locale: AR, max_pages: 2)...
[INFO] 📥 Feed RSS recebido (162117 bytes).
[INFO] 📰 20 artigos extraídos do feed XML.
[INFO] 🔗 Decodificando 20 URLs do Google News para os portais reais...
[INFO] ✅ 20/20 URLs resolvidas com sucesso para os domínios de origem.
[INFO] 💾 Arquivo salvo com sucesso: 'out/river_plate.json' (20 notícias).
```
#### 2. Cruzeiro (Brasil / Português / Formatado no Terminal) # Cruzeiro (Brasil / Português / Formatado no Terminal)
```bash
python scripts/extract_google_news.py --query "Cruzeiro" --lang pt --locale BR --pretty python scripts/extract_google_news.py --query "Cruzeiro" --lang pt --locale BR --pretty
```
#### 3. Fórmula 1 (Inglaterra / Inglês) # Filtragem com jq em modo silencioso
```bash
python scripts/extract_google_news.py --query "Formula 1" --lang en --locale GB --pretty
```
#### 4. Filtragem com `jq` em Modo Silencioso
```bash
python scripts/extract_google_news.py -q "inteligência artificial" -s | jq '.items[].url' python scripts/extract_google_news.py -q "inteligência artificial" -s | jq '.items[].url'
``` ```
--- ---
## 3. 📰 Extrator e Parser Multimotor de Artigos ## 3. 📄 Extrator e Parser Multimotor de Artigos
### Visão Geral e Tríplice Extração ### Visão Geral e Tríplice Extração
O script `scripts/extract_article_contents.py` lê os arquivos JSON gerados pelo extrator do Google News (ou qualquer lista contendo `items` com `url`), acessa cada página via **Foxcape** em modo stealth headless (reutilizando uma única sessão de navegador ativa com espera do evento `domcontentloaded`), e executa simultaneamente 3 motores especializados de extração: O script [`scripts/extract_article_contents.py`](scripts/extract_article_contents.py) lê os arquivos JSON gerados pelo extrator do Google News (ou qualquer lista contendo `items` com `url`), acessa cada página via **Foxcape** em modo stealth headless (reutilizando uma única sessão de navegador ativa com espera do evento `domcontentloaded`), e executa simultaneamente 3 motores especializados de extração:
1. **Trafilatura**: Texto principal higienizado, autores, data de publicação, categorias, tags, URL canônica e payload estruturado nativo. 1. **Trafilatura**: Texto principal higienizado, autores, data de publicação, categorias, tags, URL canônica e payload estruturado nativo.
2. **Newspaper4k**: Artigo completo, autores, imagens (`top_image` e galeria), resumo automático e palavras-chave (*keywords*) extraídas por NLP nativo. 2. **Newspaper4k**: Artigo completo, autores, imagens (`top_image` e galeria), resumo automático e palavras-chave (*keywords*) extraídas por NLP nativo.
@@ -238,20 +252,222 @@ O JSON final consolidado é salvo em `out/` com descarte de strings HTML brutas
### Exemplos de Uso ### Exemplos de Uso
#### 1. Extração Completa Automática
```bash ```bash
# Extração Completa Automática (gera out/river_plate_extracted.json)
python scripts/extract_article_contents.py -i out/river_plate.json python scripts/extract_article_contents.py -i out/river_plate.json
# Gera automaticamente out/river_plate_extracted.json
```
#### 2. Amostragem Rápida (Limit 2 Notícias) # Amostragem Rápida (Limit 2 Notícias)
```bash
python scripts/extract_article_contents.py -i out/river_plate.json --limit 2 python scripts/extract_article_contents.py -i out/river_plate.json --limit 2
# Destino Customizado e Timeout Ajustado
python scripts/extract_article_contents.py -i out/petrobras_result.json -o out/petrobras_full.json --timeout 45
``` ```
#### 3. Destino Customizado e Timeout Ajustado ---
## 4. 🎯 Seletor Determinístico de Conteúdo de Artigos
### Visão Geral e Algoritmo de Consenso ($F_1$)
O script [`scripts/select_article_extractor.py`](scripts/select_article_extractor.py) é uma ferramenta autônoma, leve e 100% determinística (baseada exclusivamente na biblioteca padrão do Python) que resolve o problema de divergência entre múltiplos extratores de texto.
O motor analisa as saídas dos candidatos ativos (`trafilatura`, `newspaper4k` e `readability`), compara a sobreposição de conteúdo através de **shingles de 5 tokens** e calcula o índice de concordância mútua via pontuação $F_1$:
$$\text{Coverage} = \frac{|\text{Shingles do Candidato} \cap \text{Consenso}|}{|\text{Consenso}|}$$
$$\text{Support} = \frac{|\text{Shingles do Candidato} \cap \text{Consenso}|}{|\text{Shingles do Candidato}|}$$
$$F_1 = \frac{2 \times \text{Coverage} \times \text{Support}}{\text{Coverage} + \text{Support}}$$
```mermaid
flowchart TD
Art[Artigo com Trafilatura, Newspaper4k e Readability] --> Class[Classificação de Viabilidade: Usable, Degraded, Unavailable]
Class --> Set[Formação do Conjunto Ativo]
Set --> Norm[Normalização NFKC & Shingles de 5 Tokens]
Norm --> Metric[Cálculo de Consenso & Métricas F1]
Metric --> Check{Existe Consenso >= 2?}
Check -- Sim --> TopScore[Avaliação do Top Score]
TopScore --> TiePool{Empate Técnico <= 0.03?}
TiePool -- Sim --> SmallestShingle[Menor Quantidade de Shingles / Menos Ruído]
SmallestShingle --> Winner[Extrator Selecionado]
TiePool -- Não --> HighestScore[Maior Pontuação F1]
HighestScore --> Winner
Check -- Não --> ZeroCons[Desempate Sem Consenso: Mediana / Max / Prioridade]
ZeroCons --> Winner
```
### Pipeline de Normalização e Shingles
A normalização ocorre em memória exclusivamente para fins comparativos:
1. **Decodificação de entidades HTML** (`html.unescape`).
2. **Descarte de imagens Markdown** (`![alt](url)`) para evitar que descrições de imagens criem falsos consensos com legendas.
3. **Preservação de links Markdown** (`[texto](url)` $\to$ `texto`).
4. **Remoção de tags HTML** preservando espaçamento entre palavras adjacentes.
5. **Normalização Unicode NFKC** e conversão para minúsculas.
6. **Colapso de espaços em branco**.
7. **Tokenização Unicode alfanumérica** com descarte de pontuações.
8. **Geração de Shingles**: Janela deslizante de 5 tokens (ou tupla única para textos curtos de 1 a 4 tokens).
### Critérios de Desempate Técnico e Resiliência
* **Empate Técnico ($\le 0.03$)**: Quando dois ou mais extratores atingem pontuações com diferença $\le 0.03$, o algoritmo seleciona aquele com **menor quantidade de shingles** (penalizando *boilerplate*, cabeçalhos ou menus excedentes).
* **Desempate Hierárquico Estrito**: Em caso de empate absoluto de pontuação e tamanho, aplica-se a hierarquia fixa:
$$\text{newspaper4k} > \text{readability} > \text{trafilatura}$$
* **Cenários de 0 Consenso**:
* 3 candidatos ativos $\to$ seleciona o de **tamanho mediano de shingles**.
* 2 candidatos ativos $\to$ seleciona o de **maior tamanho de shingles**.
* 1 candidato ativo $\to$ seleciona o único utilizável.
* Todos indisponíveis $\to$ *fallback* obrigatório em `newspaper4k`.
* **Gravação Atômica e Não-Destrutiva**: Criação de arquivo temporário com substituição atômica (`os.replace`), preservando 100% dos dados pré-existentes, ordem de artigos e propriedades originais.
### Argumentos e Flags CLI
| Parâmetro | Tipo | Padrão | Descrição |
|---|---|---|---|
| `input_file` | Posicional (obrigatório) | — | Caminho para o arquivo JSON contendo a coleção `articles`. |
| `-o, --output` | Caminho (opcional) | `<input_stem>_selected.json` | Caminho do arquivo JSON de destino. |
| `--indent` | Inteiro (opcional) | `2` | Espaços de indentação do JSON (`0` para compacto). |
| `-v, --verbose` | Flag booleana | `False` | Emite no `stderr` os detalhes de pontuação, ativos e regra de escolha por artigo. |
### Exemplos Práticos de Uso
#### 1. Execução Padrão Automática
```bash ```bash
python scripts/extract_article_contents.py -i out/petrobras_result.json -o out/petrobras_full.json --timeout 45 python scripts/select_article_extractor.py out/river_plate_extracted.json
# Gera automaticamente out/river_plate_extracted_selected.json
```
#### 2. Execução com Modo Verboso
```bash
python scripts/select_article_extractor.py out/river_plate_extracted.json --verbose
```
* **Saída no Terminal**:
```text
[Artigo #001] Extrator: newspaper4k | Motivo: technical_tie_smallest_shingles | Ativos: 3 | Consenso: 1048
[Artigo #002] Extrator: newspaper4k | Motivo: highest_score | Ativos: 3 | Consenso: 333
[Artigo #003] Extrator: readability | Motivo: highest_score | Ativos: 3 | Consenso: 778
...
{
"status": "success",
"input_file": "out/river_plate_extracted.json",
"output_file": "out/river_plate_extracted_selected.json",
"total_articles": 20,
"processed_count": 20,
"distribution": {
"newspaper4k": 9,
"readability": 9,
"trafilatura": 2
}
}
```
#### 3. Uso Programático como Módulo Python
```python
from scripts.select_article_extractor import select_article_extractor
article_data = {
"trafilatura": {"text": "River Plate venceu ontem por 3-0.", "error": None},
"newspaper4k": {"text": "River Plate venceu ontem por 3-0 no Monumental.", "error": None},
"readability": {"cleaned_text": "River Plate venceu ontem por 3-0.", "error": None},
}
result = select_article_extractor(article_data)
print("Extrator Selecionado:", result.selected_extractor.value)
print("Motivo:", result.selection_reason)
```
---
## 5. 📝 Conversor de Artigo JSON para Markdown
### Visão Geral e Estrutura do Documento
O script `scripts/convert_article_to_markdown.py` realiza a conversão de um arquivo JSON contendo exatamente um artigo (com `selected_extractor`) para um documento Markdown (`.md`) pronto para consumo editorial ou classificação downstream.
A estrutura do Markdown gerado segue estritamente o padrão:
```markdown
# Título do Artigo
Subtítulo ou descrição editorial (omitido se ausente ou igual ao título).
**Autor:** Nome do Autor 1, Nome do Autor 2
**Publicado em:** 2026-08-20T00:36:33-03:00
**Site:** Nome do Veículo
**Categoria:** Categoria 1, Categoria 2
**Tags:** Tag 1, Tag 2
**Palavras-chave:** Palavra 1, Palavra 2
**Idioma:** es
**Fonte original:** [https://exemplo.com/artigo](https://exemplo.com/artigo)
![Imagem principal](https://exemplo.com/imagem_capa.jpg)
---
Conteúdo textual do artigo convertido em Markdown.
```
### Isolamento Estrito de Extratores e Fallback
- **Isolamento Total do Corpo**: O texto e HTML do corpo vêm **exclusivamente** do extrator indicado em `selected_extractor`. Caso o extrator selecionado não possua corpo válido, o processo encerra imediatamente com erro (código `1`). Nunca ocorre fallback de corpo para outro extrator.
- **Fallback Interno**:
- `trafilatura`: usa `trafilatura.markdown`; se vazio, usa `trafilatura.text`.
- `newspaper4k`: converte `newspaper4k.article_html` para Markdown; se vazio, usa `newspaper4k.text`.
- `readability`: converte `readability.cleaned_html` para Markdown; se vazio, usa `readability.cleaned_text`.
- **Conversão HTML→Markdown**: Utiliza a biblioteca `markdownify` configurada para títulos padrão ATX (`#`, `##`, `###`), preservando negrito, itálico, listas, tabelas, citações, links e blocos de código.
### Matriz Determinística de Metadados
Os metadados editoriais são resolvidos deterministicamente consultando fontes na ordem de prioridade estrita:
1. **Título**: `SELECIONADO.title` → `input_meta.titulo` → `page_title` → `newspaper4k.title` → `trafilatura.title` → `readability.title`
2. **URL Original**: `input_meta.url` → `crawled_url` → URL canônica do selecionado → `trafilatura.canonical_url` → `newspaper4k.canonical_link`
3. **Subtítulo/Descrição**: descrição do selecionado → `trafilatura.description` → `newspaper4k.meta_description` → `input_meta.subtitulo`
4. **Autores**: autor(es) do selecionado → `newspaper4k.authors` → `trafilatura.author` → `readability.author`
5. **Data de Publicação**: data do selecionado → `newspaper4k.publish_date` → `trafilatura.date` → `input_meta.quando_publicado`
6. **Site**: site do selecionado → `trafilatura.sitename` → `newspaper4k.meta_site_name` → `trafilatura.hostname` → Hostname da URL original
7. **Categorias**: categorias do selecionado → `trafilatura.categories`
8. **Tags**: tags do selecionado → `trafilatura.tags` → `newspaper4k.tags` → `newspaper4k.meta_keywords`
9. **Palavras-chave**: `newspaper4k.keywords` → `newspaper4k.meta_keywords`
10. **Idioma**: idioma do selecionado → `trafilatura.language` → `newspaper4k.meta_lang`
11. **Imagem Principal**: imagem do selecionado → `newspaper4k.top_image` → `trafilatura.image`
### Sanitização Editorial e Deduplicação
- **Remoção de H1 Duplicado**: Se o corpo iniciar com um título H1 idêntico ao título resolvido do artigo, esse H1 inicial é removido automaticamente.
- **Filtro de Imagens**: Remove imagens com URLs relativas, vazias ou em formato `data:`. Deduplica URLs de imagem repetidas no corpo.
- **Filtro de Placeholders**: Descarta valores como `null`, `None`, `N/A`, `unknown`, `[no-author]` ou rótulos vazios.
- **Gravação Atômica**: Escrita transacional em arquivo temporário seguida de substituição com `os.replace`, garantindo integridade e nenhum resíduo em falhas.
### Argumentos e Flags CLI
| Parâmetro | Tipo | Obrigatoriedade | Padrão | Descrição |
|---|---|:---:|---|---|
| `-i, --input` | Caminho | **Sim** | — | Arquivo JSON contendo exatamente um único artigo. |
| `-o, --output` | Caminho | Não | `<input_stem>.md` | Caminho do arquivo Markdown de destino. |
### Exemplos Práticos de Uso
#### 1. Conversão Padrão
```bash
python scripts/convert_article_to_markdown.py -i out/article_001.json
# Gera automaticamente out/article_001.md
```
#### 2. Conversão com Caminho de Destino Personalizado
```bash
python scripts/convert_article_to_markdown.py \
-i out/article_001.json \
-o out/markdown/meu_artigo.md
```
#### 3. Uso Programático em Python
```python
from pathlib import Path
from scripts.convert_article_to_markdown import convert_article
out_file = convert_article(Path("out/article_001.json"), Path("out/artigo.md"))
print(f"Markdown gerado em: {out_file}")
``` ```
--- ---
@@ -260,48 +476,74 @@ python scripts/extract_article_contents.py -i out/petrobras_result.json -o out/p
```text ```text
TextNLPClassifierApp/ TextNLPClassifierApp/
├── classify.py # CLI principal do Classificador de Inerência ├── classify.py # CLI principal do Classificador de Inerência
├── scripts/ ├── scripts/
│ ├── __init__.py # Pacote utilitário de scripts │ ├── __init__.py # Pacote utilitário de scripts
│ ├── extract_google_news.py # CLI de Extração de Manchetes do Google News │ ├── extract_google_news.py # CLI de Extração de Manchetes do Google News
│ └── extract_article_contents.py # CLI de Extração e Parsing Multimotor de Artigos │ ├── extract_article_contents.py # CLI de Extração e Parsing Multimotor de Artigos
├── src/ # Módulos centrais do classificador │ ├── select_article_extractor.py # CLI de Seleção Determinística de Extrator
│ ├── classifier.py # Orquestrador de classificação (Tier 1, 2, 3) │ └── convert_article_to_markdown.py # CLI de Conversão de Artigo JSON para Markdown
│ ├── models.py # Modelos de dados e esquemas (ECPSnapshot, Decision) ├── src/ # Módulos centrais do classificador
│ ├── preprocessor.py # Normalização de texto e detecção de idioma │ ├── classifier.py # Orquestrador de classificação (Tier 1, 2, 3)
│ └── adapters/ # Adaptadores opcionais de Embeddings e LLM │ ├── models.py # Modelos de dados e esquemas (ECPSnapshot, Decision)
├── specs/ # Especificações e planos arquiteturais (Speckit) │ ├── preprocessor.py # Normalização de texto e detecção de idioma
│ └── adapters/ # Adaptadores opcionais de Embeddings e LLM
├── specs/ # Especificações e planos arquiteturais (Speckit)
│ ├── 001-multilingual-entity-classifier/ │ ├── 001-multilingual-entity-classifier/
│ ├── 002-google-news-extractor/ │ ├── 002-google-news-extractor/
│ └── 003-article-content-extractor/ # Specs da feature de extração multimotor │ ├── 003-article-content-extractor/
├── tests/ # Suíte de testes automatizados │ ├── 004-deterministic-content-selection/
│ └── 005-convert-json-markdown/ # Specs da conversão JSON para Markdown
├── tests/ # Suíte de testes automatizados
│ ├── test_classifier.py │ ├── test_classifier.py
│ ├── test_extract_google_news.py │ ├── test_extract_google_news.py
│ └── test_extract_article_contents.py # Testes do extrator de conteúdo │ ├── test_extract_article_contents.py
├── requirements.txt # Dependências do projeto │ ├── test_select_article_extractor.py
├── pyproject.toml # Configurações de ferramentas (pytest, ruff, mypy) │ ├── test_convert_article_to_markdown.py # Testes da conversão para Markdown
└── README.md # Documentação principal │ ├── test_llm_fallback.py # Testes do Tier 3 LLM Fallback
│ ├── test_e2e_text_analysis_pipeline.py # Suíte E2E do Funil de Análise e Fallback
│ └── test_classify_exhaustive_suite.py # Suíte Exaustiva de Casos Felizes/Infelizes (QA Sênior)
├── requirements.txt # Dependências do projeto
├── pyproject.toml # Configurações de ferramentas (pytest, ruff, mypy)
└── README.md # Documentação principal
``` ```
--- ---
## 🧪 Testes e Qualidade de Código ## 🧪 Testes e Qualidade de Código
O repositório possui cobertura com testes unitários, testes de integração e testes End-to-End (E2E) com requisição de rede ao vivo: O repositório possui **247 testes automatizados** com 100% de aprovação cobrindo testes unitários, de regressão, de integração, Golden Fixtures exatas, testes de sensibilidade de mutação, testes de fallback para LLM (Tier 3), validações de degradação graciosa, matriz multilíngue e testes End-to-End (E2E) via CLI subprocess:
```bash ```bash
# Executar todos os testes do projeto # Executar toda a suíte de testes do projeto (247 testes)
pytest -v pytest -v
# Executar especificamente os testes do Extrator de Notícias # Executar a Suíte Exaustiva de Classificação e Fallback (38 testes)
pytest tests/test_classify_exhaustive_suite.py -v
# Executar a Suíte E2E do Funil de Análise de Texto e Fallback para LLM
pytest tests/test_e2e_text_analysis_pipeline.py -v
# Executar os testes do Fallback para LLM (Tier 3)
pytest tests/test_llm_fallback.py -v
# Executar os testes de Conversão de Artigo para Markdown (67 testes)
pytest tests/test_convert_article_to_markdown.py -v
# Executar os testes do Seletor Determinístico
pytest tests/test_select_article_extractor.py -v
# Executar os testes do Extrator de Conteúdo Multimotor
pytest tests/test_extract_article_contents.py -v
# Executar os testes do Extrator do Google News
pytest tests/test_extract_google_news.py -v pytest tests/test_extract_google_news.py -v
# Validação e correção automática de formatação com Ruff # Validação com Ruff
ruff check --fix . ruff check .
ruff format .
# Verificação estática de tipos com Mypy # Verificação estática de tipos com Mypy
mypy scripts/ src/ mypy src/ scripts/ tests/
``` ```
--- ---
+8
View File
@@ -0,0 +1,8 @@
{
"folders": [
{
"path": "."
}
],
"settings": {}
}
+1 -1
View File
@@ -98,7 +98,7 @@ def main(argv: list[str] | None = None) -> int:
try: try:
args = parse_args(argv) args = parse_args(argv)
except SystemExit as e: except SystemExit as e:
return int(e.code) return int(e.code) if isinstance(e.code, int) else 2
ecp_path = Path(args.ecp) ecp_path = Path(args.ecp)
content_path = Path(args.content) content_path = Path(args.content)
+516
View File
@@ -0,0 +1,516 @@
# PRD — Conversão de artigo JSON para Markdown
## 1. Visão geral
O `TextNLPClassifierApp` já possui scripts para extrair artigos com Trafilatura, Newspaper4k e Readability e para selecionar deterministicamente o melhor extrator de conteúdo.
Esta feature adicionará a etapa seguinte do pipeline: receber o JSON de um único artigo, já contendo `selected_extractor`, selecionar deterministicamente os metadados disponíveis e gerar um arquivo Markdown com o conteúdo do extrator escolhido.
O arquivo `river_plate_extracted_selected(2).json` foi usado como referência de estrutura. Embora esse arquivo contenha uma coleção em `articles`, a entrada operacional desta feature será somente um objeto individual dessa coleção.
## 2. Problema
Cada artigo possui três resultados de extração com campos, formatos e níveis de preenchimento diferentes. O `selected_extractor` define qual corpo tem o maior peso e deve ser utilizado, mas metadados úteis podem estar ausentes nesse extrator e disponíveis em outro.
É necessário produzir um Markdown único e previsível sem escolher novamente o melhor conteúdo, sem usar LLM e sem depender de interpretação manual.
## 3. Objetivo
Criar um CLI Python que:
1. receba um arquivo JSON contendo exatamente um artigo;
2. valide os campos obrigatórios;
3. use exclusivamente o `selected_extractor` para obter o corpo do artigo;
4. selecione título, URL original e metadados opcionais por regras determinísticas;
5. converta o corpo HTML para Markdown quando necessário;
6. grave um arquivo `.md` legível, consistente e pronto para as etapas posteriores do pipeline.
## 4. História do usuário
Como operador do pipeline de conteúdo, quero converter o JSON selecionado de um artigo em um arquivo Markdown para que o conteúdo e os melhores metadados disponíveis possam ser consumidos pelas etapas seguintes do sistema.
## 5. Escopo
### 5.1 Incluído
- CLI Python.
- Leitura de um arquivo JSON com um único artigo.
- Suporte a `trafilatura`, `newspaper4k` e `readability` como valores de `selected_extractor`.
- Seleção determinística de metadados.
- Conversão de HTML para Markdown.
- Uso direto do Markdown já gerado pela Trafilatura quando disponível.
- Geração de um único arquivo `.md` por execução.
- Validação de entrada, saída e erros.
- Gravação atômica do arquivo de saída.
- Testes unitários, testes do CLI e arquivos de resultado esperado.
### 5.2 Fora do escopo
- Receber o objeto raiz com o array `articles`.
- Processar vários artigos em uma execução.
- Executar novamente Trafilatura, Newspaper4k ou Readability.
- Calcular ou alterar `selected_extractor`.
- Comparar, combinar ou complementar o corpo com conteúdo de outro extrator.
- Usar LLM, embeddings ou qualquer seleção probabilística.
- Fazer novas requisições HTTP.
- Baixar ou armazenar imagens.
- Limpar semanticamente publicidade, recomendações, overlays ou outros blocos editoriais presentes no corpo selecionado.
- Criar API, banco de dados, fila, interface gráfica ou integração externa.
- Alterar o JSON recebido.
## 6. Contrato de entrada
### 6.1 Formato
A entrada será um arquivo JSON UTF-8 cujo objeto raiz representa um único item do array `articles` observado no arquivo de referência.
Campos esperados no objeto:
| Campo | Tipo esperado | Obrigatoriedade | Uso |
|---|---|---:|---|
| `selected_extractor` | string | Obrigatório | Define a única fonte permitida para o corpo. |
| `input_meta` | object | Opcional | Fornece principalmente URL original, título e data de fallback. |
| `crawled_url` | string | Opcional | URL de fallback. |
| `page_title` | string | Opcional | Título de fallback. |
| `trafilatura` | object | Condicional | Obrigatório quando selecionado; opcional nos demais casos. |
| `newspaper4k` | object | Condicional | Obrigatório quando selecionado; opcional nos demais casos. |
| `readability` | object | Condicional | Obrigatório quando selecionado; opcional nos demais casos. |
### 6.2 Valores aceitos para `selected_extractor`
- `trafilatura`
- `newspaper4k`
- `readability`
Qualquer outro valor deve invalidar a entrada.
### 6.3 Campos obrigatórios após a resolução
O processamento somente será bem-sucedido se for possível resolver:
- título não vazio;
- URL original absoluta com protocolo `http` ou `https`;
- corpo não vazio pertencente ao `selected_extractor`.
Os demais campos são opcionais e nunca devem impedir a geração do Markdown.
## 7. Contrato de saída
### 7.1 Arquivo
- Formato: Markdown UTF-8.
- Quantidade: um arquivo por execução.
- Nome padrão: mesmo nome-base do JSON de entrada, substituindo `.json` por `.md`.
- Caminho alternativo: informado por `-o` ou `--output`.
- Escrita: arquivo temporário seguido de substituição atômica do destino.
### 7.2 Estrutura do Markdown
O Markdown deve seguir esta ordem:
```markdown
# Título do artigo
Subtítulo ou descrição, quando disponível.
**Autor:** Nome do autor
**Publicado em:** 2026-08-20T00:36:33-03:00
**Site:** Nome do site
**Categoria:** Categoria 1, Categoria 2
**Tags:** Tag 1, Tag 2
**Palavras-chave:** Palavra 1, Palavra 2
**Idioma:** es
**Fonte original:** [https://exemplo.com/artigo](https://exemplo.com/artigo)
![Imagem principal](https://exemplo.com/imagem.jpg)
---
Conteúdo do artigo em Markdown.
```
Regras de apresentação:
- título, URL original e corpo sempre devem aparecer;
- cada linha opcional deve ser completamente omitida quando não houver valor válido;
- nenhum placeholder como `null`, `None`, `N/A`, `unknown` ou `[no-author]` deve aparecer;
- o subtítulo deve ser omitido quando for igual ao título após normalização;
- a imagem principal deve ser omitida quando não possuir URL absoluta `http` ou `https`;
- o nome do `selected_extractor` não deve ser exibido no documento;
- os metadados técnicos internos do JSON não devem ser exibidos.
## 8. Regras funcionais
### RF-001 — Receber um único artigo
O CLI deve aceitar somente um objeto individual de artigo. Um objeto contendo `articles` deve ser rejeitado, pois o processamento em lote não pertence a esta feature.
### RF-002 — Respeitar o extrator selecionado
O corpo deve vir exclusivamente do extrator indicado em `selected_extractor`. A ausência de corpo utilizável nesse extrator deve encerrar o processamento com erro. O sistema não pode trocar silenciosamente para outro extrator.
### RF-003 — Resolver o corpo dentro do extrator selecionado
| `selected_extractor` | Fonte principal | Fallback do mesmo extrator | Tratamento |
|---|---|---|---|
| `trafilatura` | `trafilatura.markdown` | `trafilatura.text` | Usar o Markdown diretamente; o texto puro já é Markdown válido. |
| `newspaper4k` | `newspaper4k.article_html` | `newspaper4k.text` | Converter o HTML; usar texto puro somente quando o HTML estiver vazio. |
| `readability` | `readability.cleaned_html` | `readability.cleaned_text` | Converter o HTML; usar texto puro somente quando o HTML estiver vazio. |
O fallback ocorre somente entre representações do mesmo extrator selecionado.
### RF-004 — Selecionar metadados deterministicamente
Para cada campo, o sistema deve:
1. percorrer as fontes na ordem definida neste PRD;
2. normalizar e validar cada candidato;
3. selecionar o primeiro candidato válido;
4. não consultar as fontes restantes após a seleção;
5. omitir o campo se nenhum candidato opcional for válido.
A mesma entrada deve sempre gerar a mesma seleção e o mesmo arquivo.
### RF-005 — Priorizar metadados por campo
`SELECIONADO` representa o campo equivalente dentro do objeto indicado por `selected_extractor`. Quando o extrator não possuir o campo, essa posição é ignorada.
| Campo de saída | Ordem de prioridade |
|---|---|
| Título | `SELECIONADO.title` → `input_meta.titulo` → `page_title` → `newspaper4k.title` → `trafilatura.title` → `readability.title` |
| URL original | `input_meta.url` → `crawled_url` → URL canônica do selecionado → `trafilatura.canonical_url` → `newspaper4k.canonical_link` |
| Subtítulo/descrição | descrição do selecionado → `trafilatura.description` → `newspaper4k.meta_description` → `input_meta.subtitulo` |
| Autores | autor(es) do selecionado → `newspaper4k.authors` → `trafilatura.author` → `readability.author` |
| Data de publicação | data do selecionado → `newspaper4k.publish_date` → `trafilatura.date` → `input_meta.quando_publicado` |
| Site | site do selecionado → `trafilatura.sitename` → `newspaper4k.meta_site_name` → `trafilatura.hostname` → hostname da URL original |
| Categorias | categorias do selecionado → `trafilatura.categories` |
| Tags | tags do selecionado → `trafilatura.tags` → `newspaper4k.tags` → `newspaper4k.meta_keywords` |
| Palavras-chave | `newspaper4k.keywords` → `newspaper4k.meta_keywords` |
| Idioma | idioma do selecionado → `trafilatura.language` → `newspaper4k.meta_lang` |
| Imagem principal | imagem do selecionado → `newspaper4k.top_image` → `trafilatura.image` |
Mapeamento dos campos equivalentes do extrator selecionado:
| Informação | Trafilatura | Newspaper4k | Readability |
|---|---|---|---|
| Título | `title` | `title` | `title` |
| Descrição | `description` | `meta_description` | Não disponível |
| Autores | `author` | `authors` | `author` |
| Data | `date` | `publish_date` | Não disponível |
| Site | `sitename` | `meta_site_name` | Não disponível |
| Categorias | `categories` | Não disponível | Não disponível |
| Tags | `tags` | `tags` | Não disponível |
| Idioma | `language` | `meta_lang` | Não disponível |
| Imagem principal | `image` | `top_image` | Não disponível |
| URL canônica | `canonical_url` | `canonical_link` | Não disponível |
### RF-006 — Normalizar valores escalares
Antes da validação, toda string candidata deve:
- ter entidades HTML decodificadas;
- remover espaços no início e no fim;
- colapsar sequências internas de espaços em um único espaço;
- ser considerada ausente quando vazia ou quando corresponder, sem diferença entre maiúsculas e minúsculas, a um placeholder conhecido: `null`, `none`, `n/a`, `unknown`, `[no-author]` ou `no-author`.
### RF-007 — Normalizar listas
Autores, categorias, tags e palavras-chave podem chegar como lista ou string. O sistema deve:
- aceitar lista de strings;
- aceitar string única;
- separar strings com múltiplos valores apenas por ponto e vírgula;
- normalizar cada item conforme RF-006;
- descartar autor iniciado por `http://`, `https://` ou `www.`;
- eliminar duplicatas sem diferenciar maiúsculas de minúsculas, preservando a primeira grafia e a ordem original;
- considerar a fonte inválida quando nenhum item válido restar;
- usar somente a primeira fonte da tabela de prioridade que resultar em lista válida, sem unir listas de fontes diferentes.
### RF-008 — Normalizar datas
- Aceitar datas ISO 8601 e RFC 2822 observadas na entrada de referência.
- Emitir ISO 8601.
- Preservar o fuso horário informado.
- Emitir somente `YYYY-MM-DD` quando a fonte fornecer apenas a data.
- Considerar inválida uma data que não possa ser interpretada e continuar para a próxima fonte de prioridade.
### RF-009 — Validar URLs
- Aceitar somente URLs absolutas com protocolo `http` ou `https`.
- Não fazer requisições para validar existência ou disponibilidade.
- Não aceitar `data:`, `javascript:`, caminhos relativos ou strings sem hostname.
### RF-010 — Converter HTML para Markdown
A conversão deve usar a biblioteca Python `markdownify`, configurada para produzir títulos no padrão ATX (`#`, `##`, `###`).
Devem ser preservados, quando presentes no HTML selecionado:
- parágrafos;
- títulos e subtítulos estruturais;
- listas ordenadas e não ordenadas;
- negrito e itálico;
- links;
- citações;
- blocos de código;
- tabelas suportadas pela biblioteca;
- imagens válidas do próprio corpo.
A escolha de `markdownify` é intencional: a necessidade é exclusivamente converter HTML para Markdown. O Microsoft MarkItDown suporta HTML, mas atende vários outros formatos e acrescentaria uma abstração mais ampla do que a requerida por esta feature.
### RF-011 — Tratar imagens do corpo
- Preservar imagens convertidas do corpo somente quando o destino for uma URL absoluta `http` ou `https`.
- Remover imagens com URL vazia, relativa ou `data:`.
- Eliminar repetições exatas da mesma URL de imagem, preservando a primeira ocorrência.
- Não adicionar ao corpo a coleção `newspaper4k.images`.
- Não baixar, redimensionar ou validar remotamente imagens.
### RF-012 — Evitar título duplicado
Após a conversão do corpo, o sistema deve remover o primeiro título H1 do corpo somente quando ele for igual ao título resolvido após decodificação de HTML, normalização de espaços e comparação sem diferença entre maiúsculas e minúsculas.
Outros títulos do conteúdo devem ser preservados.
### RF-013 — Normalizar o Markdown final
O arquivo final deve:
- usar quebra de linha `LF`;
- terminar com exatamente uma quebra de linha;
- eliminar espaços no final das linhas;
- limitar sequências de linhas vazias a no máximo duas;
- não conter tags HTML remanescentes geradas apenas pela estrutura do documento;
- preservar o texto, a pontuação e os caracteres Unicode do conteúdo selecionado.
### RF-014 — Não gerar saída parcial
Se ocorrer qualquer erro antes da conclusão, o arquivo de destino existente deve permanecer intacto e nenhum arquivo temporário deve permanecer no diretório de saída.
## 9. Interface CLI
### 9.1 Script
`scripts/convert_article_to_markdown.py`
### 9.2 Argumentos
| Parâmetro | Tipo | Obrigatoriedade | Padrão | Descrição |
|---|---|---:|---|---|
| `-i`, `--input` | caminho | Obrigatório | — | JSON contendo um único artigo. |
| `-o`, `--output` | caminho | Opcional | `<input_stem>.md` | Arquivo Markdown de destino. |
Não devem ser adicionadas flags sem requisito funcional neste PRD.
### 9.3 Exemplos
```bash
python scripts/convert_article_to_markdown.py -i out/article_001.json
```
Resultado: `out/article_001.md`.
```bash
python scripts/convert_article_to_markdown.py \
-i out/article_001.json \
-o out/markdown/article_001.md
```
### 9.4 Saída do processo
- Código `0`: arquivo gerado com sucesso.
- Código `2`: argumentos inválidos, conforme comportamento do `argparse`.
- Código `1`: erro de leitura, validação, conversão ou gravação.
- Mensagens de erro e confirmação devem ir para `stderr`.
- O conteúdo Markdown não deve ser impresso no terminal quando houver arquivo de saída.
## 10. Tratamento de erros
O processamento deve falhar de forma clara nos seguintes casos:
| Situação | Comportamento esperado |
|---|---|
| Arquivo não encontrado ou ilegível | Encerrar com código `1` e informar o caminho. |
| JSON inválido | Encerrar com código `1` e informar que a entrada não é JSON válido. |
| Raiz diferente de objeto | Encerrar com código `1`. |
| Entrada contém `articles` | Encerrar com código `1` e informar que o CLI aceita um único artigo. |
| `selected_extractor` ausente ou desconhecido | Encerrar com código `1`. |
| Objeto do extrator selecionado ausente | Encerrar com código `1`. |
| Corpo do extrator selecionado vazio | Encerrar com código `1`; não usar outro extrator. |
| Título não resolvido | Encerrar com código `1`. |
| URL original não resolvida ou inválida | Encerrar com código `1`. |
| Metadado opcional inválido | Ignorar o candidato e tentar o próximo; omitir se todos falharem. |
| Falha na conversão | Encerrar com código `1`. |
| Falha na gravação | Encerrar com código `1` sem alterar o destino anterior. |
As mensagens não devem imprimir o conteúdo integral do artigo.
## 11. Requisitos não funcionais
### RNF-001 — Determinismo
A mesma entrada e a mesma versão das dependências devem produzir exatamente o mesmo arquivo Markdown.
### RNF-002 — Compatibilidade
A feature deve manter as versões de Python declaradas como suportadas pelo projeto e funcionar nos sistemas operacionais já suportados pelo repositório.
### RNF-003 — Execução local
O processamento deve ocorrer inteiramente em memória local, sem rede, browser, LLM ou serviço externo.
### RNF-004 — Integridade
A entrada não deve ser modificada. A gravação de saída deve ser atômica.
### RNF-005 — Manutenibilidade
As regras de resolução de campos e as regras de conversão devem ser isoladas em funções testáveis, sem duplicação entre o CLI e o uso interno.
### RNF-006 — Qualidade
O código deve atender aos gates já adotados pelo projeto: Ruff, Mypy, Pytest e SonarQube.
## 12. Critérios de aceite
### CA-001 — Trafilatura selecionada
**Dado** um artigo com `selected_extractor` igual a `trafilatura` e `trafilatura.markdown` preenchido
**Quando** o CLI for executado
**Então** o corpo do arquivo deve vir de `trafilatura.markdown` e nenhum corpo dos demais extratores deve ser incorporado.
### CA-002 — Newspaper4k selecionado
**Dado** um artigo com `selected_extractor` igual a `newspaper4k` e `newspaper4k.article_html` preenchido
**Quando** o CLI for executado
**Então** esse HTML deve ser convertido para Markdown preservando sua estrutura editorial.
### CA-003 — Readability selecionado
**Dado** um artigo com `selected_extractor` igual a `readability` e `readability.cleaned_html` preenchido
**Quando** o CLI for executado
**Então** esse HTML deve ser convertido para Markdown preservando sua estrutura editorial.
### CA-004 — Fallback dentro do extrator
**Dado** um extrator selecionado cujo campo estruturado esteja vazio, mas cujo campo de texto puro esteja preenchido
**Quando** o CLI for executado
**Então** o texto puro do mesmo extrator deve ser usado.
### CA-005 — Proibição de fallback de corpo entre extratores
**Dado** um extrator selecionado sem HTML, Markdown ou texto utilizável e outro extrator com conteúdo
**Quando** o CLI for executado
**Então** o processamento deve falhar sem utilizar o outro extrator.
### CA-006 — Metadado vindo de outro extrator
**Dado** um artigo cujo extrator selecionado não possua autor e outro extrator possua autor válido
**Quando** o CLI for executado
**Então** o primeiro autor válido conforme a prioridade deve aparecer no Markdown.
### CA-007 — Obrigatórios presentes
**Dado** um artigo válido
**Quando** o Markdown for gerado
**Então** ele deve conter título, URL original e corpo não vazio.
### CA-008 — Opcionais ausentes
**Dado** um artigo sem metadados opcionais válidos
**Quando** o Markdown for gerado
**Então** nenhuma linha vazia de metadado ou placeholder deve ser exibida.
### CA-009 — Imagens inválidas
**Dado** um corpo com imagens `data:`, vazias ou relativas
**Quando** o conteúdo for convertido
**Então** essas imagens devem ser removidas do Markdown.
### CA-010 — Título repetido no corpo
**Dado** um corpo que começa com H1 igual ao título resolvido
**Quando** o Markdown for montado
**Então** deve existir somente um H1 com esse título no arquivo final.
### CA-011 — Entrada em lote rejeitada
**Dado** o arquivo completo de referência contendo `articles`
**Quando** ele for passado diretamente ao CLI
**Então** o processamento deve falhar informando que a entrada esperada é um único artigo.
### CA-012 — Determinismo
**Dado** o mesmo JSON processado duas vezes com as mesmas dependências
**Quando** os arquivos forem comparados byte a byte
**Então** eles devem ser idênticos.
### CA-013 — Gravação segura
**Dado** um arquivo de destino preexistente e uma falha durante o processamento
**Quando** o CLI encerrar
**Então** o arquivo preexistente deve continuar inalterado.
## 13. Estratégia de testes
### 13.1 Testes unitários
- resolução de cada campo conforme a ordem de prioridade;
- normalização de strings e placeholders;
- normalização, deduplicação e seleção de listas;
- parsing e padronização de datas ISO 8601 e RFC 2822;
- validação de URLs;
- seleção do corpo para cada extrator;
- fallback de representação dentro do mesmo extrator;
- bloqueio do fallback de corpo para outro extrator;
- conversão dos principais elementos HTML;
- remoção de imagens inválidas e duplicadas;
- remoção somente do H1 inicial duplicado;
- normalização final de espaços e quebras de linha.
### 13.2 Testes de integração do CLI
- geração com caminho padrão;
- geração com `--output`;
- códigos de saída `0`, `1` e `2`;
- mensagens em `stderr`;
- rejeição do JSON com coleção `articles`;
- preservação do destino em caso de falha;
- codificação UTF-8 com caracteres acentuados.
### 13.3 Casos de resultado esperado
Devem existir pelo menos três fixtures válidas, uma para cada valor de `selected_extractor`, acompanhadas dos respectivos arquivos Markdown esperados. A comparação deve ser exata.
Também devem existir fixtures inválidas cobrindo:
- JSON corrompido;
- `selected_extractor` ausente;
- extrator desconhecido;
- corpo selecionado vazio;
- título ausente em todas as fontes;
- URL ausente ou inválida em todas as fontes;
- objeto raiz contendo `articles`.
## 14. Definition of Done
A feature será considerada concluída quando:
- o script `scripts/convert_article_to_markdown.py` estiver implementado;
- `markdownify` estiver declarada nas dependências do projeto;
- todos os requisitos funcionais e critérios de aceite estiverem cobertos;
- os testes unitários e de integração estiverem passando;
- as três fixtures de extratores produzirem exatamente os Markdown esperados;
- Ruff não apontar erros;
- Mypy não apontar erros;
- o conjunto completo de testes do projeto permanecer aprovado;
- o Quality Gate do SonarQube estiver aprovado;
- o README documentar a nova etapa, os argumentos e exemplos do CLI;
- nenhuma funcionalidade fora do escopo tiver sido adicionada.
## 15. Dependência técnica escolhida
- Biblioteca: [`markdownify`](https://github.com/matthewwithanm/python-markdownify)
- Finalidade: conversão direta de HTML para Markdown em Python.
- Alternativa avaliada: [`Microsoft MarkItDown`](https://github.com/microsoft/markitdown).
- Decisão: não usar MarkItDown nesta feature porque seu escopo de conversão multiformato excede a necessidade de HTML para Markdown.
+279
View File
@@ -0,0 +1,279 @@
# PRD — Seleção determinística da biblioteca de extração de conteúdo
**Versão:** 1.0
**Data:** 20/08/2026
**Status:** Pronto para implementação
## 1. Contexto
Cada artigo é processado pelas bibliotecas Trafilatura, Newspaper4k e Readability a partir do mesmo HTML. O resultado é consolidado em um arquivo JSON que contém, para cada artigo, as saídas das três bibliotecas.
É necessário escolher deterministicamente uma única biblioteca por artigo. A escolha deve ocorrer mesmo quando todos os resultados forem ruins. O processamento não pode retornar estado ambíguo nem deixar um artigo sem seleção.
## 2. Objetivo
Receber um arquivo JSON no formato do arquivo de referência, selecionar a melhor saída de extração disponível para cada artigo e gerar um novo arquivo JSON com os mesmos dados, acrescentando somente a chave `selected_extractor` em cada item de `articles`.
## 3. Escopo
### 3.1 Incluído
- Ler um arquivo JSON com uma coleção `articles`.
- Comparar as saídas de Trafilatura, Newspaper4k e Readability de cada artigo.
- Escolher obrigatoriamente uma das três bibliotecas.
- Adicionar `selected_extractor` em cada artigo.
- Gerar um novo arquivo JSON.
- Preservar os dados e a ordem dos artigos recebidos.
### 3.2 Fora do escopo
- Baixar ou renderizar páginas.
- Verificar se as bibliotecas receberam o mesmo HTML.
- Executar novamente as bibliotecas de extração.
- Limpar, recortar, combinar ou reescrever o conteúdo extraído.
- Gerar Markdown.
- Usar LLM, embeddings ou regras específicas por domínio.
- Alterar qualquer campo existente no JSON.
- Adicionar métricas, justificativas ou outras chaves ao arquivo de saída.
## 4. Premissas
- As três bibliotecas processaram exatamente o mesmo HTML.
- A entrada segue a estrutura do JSON de referência.
- A seleção é executada individualmente para cada artigo.
- O algoritmo deve sempre produzir uma escolha, inclusive em situações sem concordância entre as bibliotecas.
## 5. Entrada
### 5.1 Arquivo
- Formato: JSON válido.
- A raiz deve conter `articles` como uma lista.
- Cada item de `articles` representa um artigo.
### 5.2 Conteúdos comparados
| Biblioteca | Campo usado na comparação |
| ----------- | -------------------------- |
| Trafilatura | `trafilatura.text` |
| Newspaper4k | `newspaper4k.text` |
| Readability | `readability.cleaned_text` |
Os demais campos, incluindo título, descrição, resumo, palavras-chave, HTML estruturado e metadados, não participam da seleção.
### 5.3 Estado de um candidato
Para cada biblioteca, o candidato é classificado em um dos seguintes estados:
| Estado | Condição |
| ------------ | ---------------------------------------------------------------------------------- |
| Utilizável | Campo de conteúdo é uma string não vazia após normalização e `error` é nulo |
| Degradado | Campo de conteúdo é uma string não vazia após normalização, mas `error` não é nulo |
| Indisponível | Campo ausente, nulo, de tipo diferente de string ou vazio após normalização |
O campo de erro considerado é `trafilatura.error`, `newspaper4k.error` ou `readability.error`, conforme a biblioteca.
## 6. Saída
### 6.1 Arquivo
O arquivo de entrada nunca deve ser alterado. Deve ser criado um novo arquivo com o nome:
`<nome_original_sem_extensão>_selected.json`
Exemplo: `river_plate_extracted(2).json` gera `river_plate_extracted(2)_selected.json`.
### 6.2 Alteração permitida
Cada item de `articles` deve receber exatamente uma nova chave no mesmo nível de `trafilatura`, `newspaper4k` e `readability`:
`selected_extractor`
Valores permitidos:
- `trafilatura`
- `newspaper4k`
- `readability`
Não são permitidos `null`, string vazia, `ambiguous` ou qualquer outro valor.
### 6.3 Preservação da entrada
- Todas as chaves e valores existentes devem permanecer semanticamente idênticos.
- A ordem dos itens de `articles` deve ser preservada.
- Nenhum artigo pode ser adicionado ou removido.
- Espaçamento, indentação e ordem textual das chaves do JSON não fazem parte do contrato, pois o arquivo pode ser serializado novamente.
- Caso `selected_extractor` já exista, seu valor deve ser recalculado e substituído.
## 7. Algoritmo determinístico de seleção
### 7.1 Formar o conjunto ativo
Para cada artigo:
1. Identificar os candidatos utilizáveis.
2. Se existir pelo menos um utilizável, considerar somente os utilizáveis.
3. Se não existir utilizável, considerar os candidatos degradados.
4. Se não existir candidato utilizável nem degradado, selecionar `newspaper4k` pelo desempate final obrigatório.
5. Se o conjunto ativo possuir somente um candidato, selecioná-lo imediatamente.
### 7.2 Normalizar os conteúdos
A normalização serve apenas para comparação e não modifica o JSON de saída.
Para cada candidato ativo:
1. Decodificar entidades HTML.
2. Remover marcação HTML e Markdown, preservando o texto visível.
3. Em links, preservar o texto e remover o endereço.
4. Aplicar normalização Unicode NFKC.
5. Converter o texto para minúsculas.
6. Substituir toda sequência de espaços, tabulações ou quebras de linha por um único espaço.
7. Tokenizar mantendo letras e números Unicode.
8. Desconsiderar pontuação.
Nenhuma palavra ou trecho pode ser removido por interpretação semântica.
### 7.3 Gerar shingles
- Gerar a sequência ordenada de tokens de cada candidato.
- Formar o conjunto de todas as janelas consecutivas de cinco tokens.
- Quando o candidato possuir entre um e quatro tokens, usar a sequência completa como um único shingle.
- Candidato sem token é indisponível e não participa do conjunto ativo.
### 7.4 Construir o consenso
O consenso é o conjunto de shingles presentes em pelo menos dois candidatos ativos.
Para cada candidato ativo, calcular:
**Cobertura:**
`coverage = quantidade de shingles do consenso presentes no candidato / quantidade total de shingles do consenso`
**Suporte:**
`support = quantidade de shingles do candidato presentes no consenso / quantidade total de shingles do candidato`
**Pontuação:**
`score = 2 × coverage × support / (coverage + support)`
Quando `coverage + support` for zero, a pontuação será zero.
### 7.5 Selecionar quando existe consenso
1. Ordenar os candidatos por `score`, do maior para o menor.
2. Identificar o maior `score`.
3. Considerar empate técnico todo candidato cuja diferença para o maior `score` seja menor ou igual a `0,03`.
4. Se houver apenas um candidato no empate técnico, selecioná-lo.
5. Se houver empate técnico, selecionar o candidato com a menor quantidade de shingles.
6. Se a quantidade de shingles também empatar, aplicar a prioridade final:
1. `newspaper4k`
2. `readability`
3. `trafilatura`
A preferência pelo menor candidato ocorre somente no empate técnico. Nesse cenário, os candidatos possuem qualidade de concordância equivalente, e a decisão favorece menor conteúdo excedente.
### 7.6 Selecionar quando não existe consenso
Quando nenhum shingle aparece em pelo menos dois candidatos ativos:
- Com três candidatos ativos: selecionar o candidato com a quantidade mediana de shingles.
- Com dois candidatos ativos: selecionar o candidato com a maior quantidade de shingles.
- Com um candidato ativo: selecionar o único candidato.
- Em empate de quantidade: aplicar a prioridade `newspaper4k`, `readability`, `trafilatura`.
- Sem candidato ativo: selecionar `newspaper4k`.
Essas regras garantem uma escolha mesmo quando não existe concordância textual.
### 7.7 Gravar a escolha
Adicionar ou substituir `selected_extractor` no artigo com o identificador da biblioteca vencedora. Repetir o processo até que todos os itens de `articles` tenham sido processados.
## 8. Requisitos funcionais
| ID | Requisito |
| ------ | ------------------------------------------------------------------------------------------------------- |
| FR-001 | O sistema deve aceitar um arquivo JSON como entrada. |
| FR-002 | O sistema deve validar que a raiz é um objeto e que `articles` é uma lista. |
| FR-003 | O sistema deve processar todos os artigos, preservando sua ordem. |
| FR-004 | O sistema deve usar exclusivamente os três campos de conteúdo definidos no PRD para calcular a escolha. |
| FR-005 | O sistema deve executar a normalização e a comparação conforme o algoritmo deste PRD. |
| FR-006 | O sistema deve selecionar exatamente uma biblioteca por artigo. |
| FR-007 | O sistema nunca deve produzir resultado ambíguo. |
| FR-008 | O sistema deve adicionar somente `selected_extractor` em cada artigo. |
| FR-009 | O valor de `selected_extractor` deve pertencer ao catálogo fechado de valores permitidos. |
| FR-010 | O sistema deve preservar todas as chaves e valores recebidos. |
| FR-011 | O sistema deve gerar um novo arquivo e manter o arquivo original inalterado. |
| FR-012 | O sistema deve produzir a mesma seleção sempre que receber exatamente a mesma entrada. |
| FR-013 | O sistema deve recalcular `selected_extractor` quando a chave já existir. |
## 9. Tratamento de erros
| Situação | Comportamento obrigatório |
| ---------------------------------------- | ----------------------------------------------------- |
| JSON inválido | Encerrar o processamento e não gerar arquivo de saída |
| Raiz diferente de objeto | Encerrar o processamento e não gerar arquivo de saída |
| `articles` ausente ou diferente de lista | Encerrar o processamento e não gerar arquivo de saída |
| `articles` vazio | Gerar arquivo com lista vazia e sem outras alterações |
| Estrutura de uma biblioteca ausente | Tratar seu candidato como indisponível |
| Campo de conteúdo com tipo inválido | Tratar seu candidato como indisponível |
| Todas as bibliotecas indisponíveis | Selecionar `newspaper4k` |
| Falha ao gravar o arquivo | Não deixar arquivo de saída parcialmente gravado |
Um erro em um artigo não pode impedir a seleção dos demais artigos, desde que o JSON e a lista `articles` sejam válidos.
## 10. Requisitos não funcionais
| ID | Requisito |
| ------- | ----------------------------------------------------------------------------------------- |
| NFR-001 | O processamento deve ser totalmente determinístico. |
| NFR-002 | O processamento não deve realizar chamadas de rede. |
| NFR-003 | O processamento não deve depender de LLM, embeddings ou serviços externos. |
| NFR-004 | O processamento deve operar somente sobre os dados do arquivo recebido. |
| NFR-005 | A gravação do arquivo deve ser atômica: sucesso completo ou ausência do arquivo de saída. |
## 11. Critérios de aceite
1. Dado o JSON de referência com 20 artigos, o arquivo de saída contém os mesmos 20 artigos na mesma ordem.
2. Cada artigo contém exatamente um `selected_extractor` válido.
3. Nenhum artigo contém `selected_extractor` nulo, vazio ou ambíguo.
4. Todas as chaves e valores anteriores permanecem semanticamente idênticos.
5. Nenhuma chave adicional, além de `selected_extractor`, é criada.
6. O arquivo original permanece inalterado.
7. Duas execuções sobre o mesmo arquivo produzem os mesmos valores de `selected_extractor`.
8. A seleção usa somente `trafilatura.text`, `newspaper4k.text` e `readability.cleaned_text`.
9. Quando todas as saídas estiverem vazias ou indisponíveis, `selected_extractor` recebe `newspaper4k`.
10. Quando não houver consenso, o desempate segue exatamente as regras da seção 7.6.
11. Quando houver empate técnico, o desempate segue exatamente as regras da seção 7.5.
12. Um JSON inválido ou sem `articles` válido não produz arquivo parcial.
## 12. Casos obrigatórios de teste
| Caso | Condição | Resultado esperado |
| ------ | ---------------------------------------------------------------------------- | ------------------------------------------------- |
| CT-001 | Três candidatos com consenso e um vencedor claro | Selecionar o maior `score` |
| CT-002 | Dois ou mais candidatos dentro de `0,03` do maior `score` | Selecionar o de menor quantidade de shingles |
| CT-003 | Empate técnico e mesma quantidade de shingles | Aplicar prioridade final |
| CT-004 | Três candidatos sem consenso | Selecionar a quantidade mediana de shingles |
| CT-005 | Dois candidatos sem consenso | Selecionar a maior quantidade de shingles |
| CT-006 | Somente um candidato utilizável | Selecionar esse candidato |
| CT-007 | Nenhum utilizável, mas existe candidato degradado | Executar o algoritmo somente com os degradados |
| CT-008 | Todos os candidatos indisponíveis | Selecionar `newspaper4k` |
| CT-009 | Readability retorna apenas um fragmento pequeno enquanto os outros concordam | O fragmento perde por baixa cobertura do consenso |
| CT-010 | Um candidato contém o conteúdo comum e muito conteúdo excedente | O candidato perde suporte e reduz sua pontuação |
| CT-011 | Um candidato contém somente parte do conteúdo comum | O candidato perde cobertura e reduz sua pontuação |
| CT-012 | A entrada já contém `selected_extractor` | Recalcular e substituir somente essa chave |
| CT-013 | `articles` está vazio | Gerar saída válida com `articles` vazio |
| CT-014 | JSON inválido | Não gerar saída |
## 13. Definition of Done
- Todos os requisitos funcionais foram implementados.
- Todos os casos obrigatórios de teste foram automatizados e aprovados.
- O JSON de referência com 20 artigos é processado integralmente.
- A saída contém somente a inclusão de `selected_extractor` em cada artigo.
- O arquivo original permanece inalterado.
- Execuções repetidas sobre a mesma entrada produzem as mesmas escolhas.
- Não existe caminho de execução que produza `ambiguous`, `null` ou artigo sem seleção.
+143
View File
@@ -0,0 +1,143 @@
{
"target_entity_id": "ecp_river_plate",
"target_name": "Club Atlético River Plate",
"canonical_name": "Club Atlético River Plate",
"domain": "Futebol / Esportes",
"aliases": [
"Club Atlético River Plate",
"River Plate",
"C.A. River Plate",
"CA River Plate",
"River",
"CARP",
"C.A.R.P.",
"El Millonario",
"Los Millonarios",
"La Banda",
"La Banda Roja",
"El Más Grande",
"Millo",
"El Millo"
],
"anchors": [
"fútbol",
"futebol",
"football",
"Copa Libertadores",
"Libertadores",
"Copa Sudamericana",
"Sudamericana",
"Liga Profesional",
"Liga Profesional de Fútbol",
"Primera División",
"Copa Argentina",
"AFA",
"Conmebol",
"Superclásico",
"Monumental",
"Estadio Monumental",
"El Monumental",
"Antonio Vespucio Liberti",
"Núñez",
"River Camp",
"Marcelo Gallardo",
"Gallardo",
"Martín Demichelis",
"Demichelis",
"Eduardo Coudet",
"Chacho Coudet",
"Coudet",
"Enzo Francescoli",
"Francescoli",
"Franco Armani",
"Armani",
"Germán Pezzella",
"Pezzella",
"Nicolás Otamendi",
"Otamendi",
"Ángel Correa",
"Angel Correa",
"Rafael Santos Borré",
"Santos Borré",
"Santiago Beltrán",
"Lucas Martínez Quarta",
"Martínez Quarta",
"Lautaro Rivero",
"Facundo González",
"Tobías Andrada",
"Fausto Vera",
"Aníbal Moreno",
"Tomás Galván",
"Thiago Almada",
"Joaquín Freitas",
"Valentín Lucero",
"Juan Meza",
"Boca Juniors",
"Independiente Santa Fe"
],
"negative_anchors": [
"River Plate de Montevideo",
"Club Atlético River Plate (Uruguay)",
"River Plate de Asunción",
"Club River Plate (Asunción)",
"River Plate de Sergipe",
"River Atlético Clube",
"River de Piauí",
"Rio River Plate",
"Rio da Prata",
"Bacia do Rio da Prata",
"Batalha do Rio da Prata",
"Estuário do Rio da Prata"
],
"graph_version": "1.0.0",
"related_entities": [
{
"entity_id": "boca_juniors",
"name": "Club Atlético Boca Juniors",
"relation_type": "RIVAL_OF",
"weight": 0.9,
"aliases": ["Boca Juniors", "Boca", "Xeneize"],
"scope": "derby"
},
{
"entity_id": "estadio_monumental",
"name": "Estadio Mâs Monumental",
"relation_type": "HOME_VENUE_OF",
"weight": 0.95,
"aliases": ["Monumental", "El Monumental", "Estadio Monumental", "Antonio Vespucio Liberti"],
"scope": "venue"
},
{
"entity_id": "copa_libertadores",
"name": "Copa Libertadores",
"relation_type": "COMPETES_IN",
"weight": 0.85,
"aliases": ["Libertadores", "Conmebol Libertadores"],
"scope": "tournament"
},
{
"entity_id": "copa_sudamericana",
"name": "Copa Sudamericana",
"relation_type": "COMPETES_IN",
"weight": 0.85,
"aliases": ["Sudamericana", "Conmebol Sudamericana"],
"scope": "tournament"
},
{
"entity_id": "eduardo_coudet",
"name": "Eduardo Coudet",
"relation_type": "MANAGER_OF",
"weight": 0.8,
"aliases": ["Chacho Coudet", "Coudet"],
"scope": "staff"
},
{
"entity_id": "marcelo_gallardo",
"name": "Marcelo Gallardo",
"relation_type": "ICONIC_MANAGER_OF",
"weight": 0.8,
"aliases": ["Gallardo", "Muñeco Gallardo"],
"scope": "staff"
}
]
}
+68 -9
View File
@@ -44,10 +44,10 @@
"42": "1. Input Schemas", "42": "1. Input Schemas",
"43": "2. Basic CLI Usage Examples", "43": "2. Basic CLI Usage Examples",
"44": "2. Standard Streams & Exit Codes", "44": "2. Standard Streams & Exit Codes",
"45": "classifier.py", "45": "ClassificationResult",
"46": "InherenceClassifier", "46": "test_adversarial.py",
"47": "detect_language", "47": "InherenceClassifier",
"48": "ClassificationError", "48": "test_convert_article_to_markdown.py",
"49": "content_northvolt_de.md", "49": "content_northvolt_de.md",
"50": "content_presal_pt.md", "50": "content_presal_pt.md",
"51": "content_tangential_es.md", "51": "content_tangential_es.md",
@@ -98,13 +98,13 @@
"96": "readiness.md", "96": "readiness.md",
"97": "🧠 TextNLPClassifierApp", "97": "🧠 TextNLPClassifierApp",
"98": "Extraction Pipeline Checklist: Article Content Multi-Engine Extractor", "98": "Extraction Pipeline Checklist: Article Content Multi-Engine Extractor",
"99": "sample_rss_xml", "99": "parametrize",
"100": "main", "100": "Path",
"101": "ECPSnapshot", "101": "main",
"102": "Feature Specification: Multilingual NLP Entity Inherence Classifier (POC)", "102": "Feature Specification: Multilingual NLP Entity Inherence Classifier (POC)",
"103": "4. Requisitos Funcionais (FR)", "103": "4. Requisitos Funcionais (FR)",
"104": "Tasks: Article Content Multi-Engine Extractor", "104": "Tasks: Article Content Multi-Engine Extractor",
"105": "models.py", "105": "Tasks: Convert Article JSON to Markdown",
"106": "Implementation Plan: Article Content Multi-Engine Extractor", "106": "Implementation Plan: Article Content Multi-Engine Extractor",
"107": "2. Cenários de Validação", "107": "2. Cenários de Validação",
"108": "1. Technical Decisions & Tradeoffs", "108": "1. Technical Decisions & Tradeoffs",
@@ -113,5 +113,64 @@
"111": "Specification Quality Checklist: Multilingual NLP Entity Inherence Classifier", "111": "Specification Quality Checklist: Multilingual NLP Entity Inherence Classifier",
"112": "CLI Contract: Article Content Multi-Engine Extractor", "112": "CLI Contract: Article Content Multi-Engine Extractor",
"113": "001-multilingual-entity-classifier/spec.md", "113": "001-multilingual-entity-classifier/spec.md",
"114": "JSON Schema Contract: Article Content Multi-Engine Extractor" "114": "JSON Schema Contract: Article Content Multi-Engine Extractor",
"115": "PRD — Seleção determinística da biblioteca de extração de conteúdo",
"116": "select_article_extractor.py",
"117": "1. Text Normalization Pipeline",
"118": "Tasks: Deterministic Article Content Selection",
"119": "select_article_extractor",
"120": "process_batch",
"121": "detect_language",
"122": "test_select_article_extractor.py",
"123": "Feature Specification: Deterministic Content Selection",
"124": "2. Entity Descriptions & Fields",
"125": "Implementation Plan: Deterministic Article Content Selection",
"126": "Deterministic Content Selection Checklist: End-to-End Requirements Quality",
"127": "Quickstart: Deterministic Article Content Selection",
"128": "Specification Quality Checklist: Deterministic Content Selection",
"129": "CLI Interface Contract: Deterministic Article Content Selection",
"130": "004-deterministic-content-selection/spec.md",
"131": "convert_article_to_markdown.py",
"132": "8. Regras funcionais",
"133": "12. Critérios de aceite",
"134": "PRD — Conversão de artigo JSON para Markdown",
"135": "resolve_article_body",
"136": "Implementation Plan: Convert Article JSON to Markdown",
"137": "2. Technical Decisions & Research Findings",
"138": "Feature Specification: Convert Article JSON to Markdown",
"139": "Markdown Conversion Checklist: End-to-End Requirements Quality",
"140": "convert_article",
"141": "005-convert-json-markdown/plan.md",
"142": "Quickstart: Convert Article JSON to Markdown",
"143": "1. Domain Entities & Schemas",
"144": "11. Requisitos não funcionais",
"145": "parse_arguments",
"146": "Specification Quality Checklist: Convert Article JSON to Markdown",
"147": "CLI Contract: `convert_article_to_markdown.py`",
"148": "9. Interface CLI",
"149": "sample_rss_xml",
"150": "13. Estratégia de testes",
"151": "6. Contrato de entrada",
"152": "ECPSnapshot",
"153": "convert_html_to_markdown",
"154": "JSON Schema Contract: Deterministic Article Content Selection",
"155": "5. Escopo",
"156": "Los puntajes de River vs. Independiente Santa Fe, por la Copa Sudamericana - TyC Sports",
"157": "valid_newspaper4k.md",
"158": "valid_readability.md",
"159": "test_normalize_list_author_url_filtering",
"160": "test_normalize_date_invalid_and_placeholders",
"161": "test_normalize_list_deduplication_preserves_case_and_order",
"162": "test_normalize_date_iso_8601_variants",
"163": "test_metadata_priority_original_url_all_fallbacks",
"164": "test_normalize_scalar_non_string_types",
"165": "test_e2e_text_analysis_pipeline.py",
"166": "remove_duplicate_initial_h1",
"167": "test_normalize_scalar_whitespace_collapsing",
"168": "LLMFallbackAdapter",
"169": "test_funnel_cli_subprocess_end_to_end",
"170": "test_models.py",
"171": "extract_evidence_snippets",
"172": ".classify",
"173": "🧪 Documentação da Suíte de Testes Automatizados"
} }
+1 -1
View File
@@ -1 +1 @@
{"0": "36bdb6f09c457f7c", "1": "8c5bf6244cf710c6", "2": "efbcc9c62a3ee78b", "3": "8599153989b07faa", "4": "b5952a1f7fee9f20", "5": "5b8462a3f82d188c", "6": "80f79e9e2011a3e3", "7": "4654167fd211d027", "8": "50acfa00fe353440", "9": "c6d2f770737823f1", "10": "44f2ca451aea24be", "11": "feaac5ab67a8c17a", "12": "b71bd92e5edbf2e0", "13": "219d65ba6d2689e4", "14": "8e30bb8112fd02d1", "15": "03906ab80b99db85", "16": "5d51c60ba1bc2be0", "17": "a1da914f522dcd21", "18": "fbad840891b90569", "19": "0686ff2d6fe29fb3", "20": "060baa9e1924b465", "21": "a5c8f2c3080b8243", "22": "0d76852f1d29eeb1", "23": "6ff68619f2d72924", "24": "3da11675eee7ec46", "25": "a6696589e9556f97", "26": "6c752999e8a4d4b6", "27": "2d4e13ea2111d750", "28": "4b60cb0ee1ac186a", "29": "f56fbca9bb8235ec", "30": "c7beed940704509f", "31": "38be2d254fb31ae8", "32": "ee5596fcf7e7c0b3", "33": "e4d4e0a440bc599f", "34": "c897e49c001acdae", "35": "3aad272a2cf5d495", "36": "0a197439d306b956", "37": "f43acf5c8b1329af", "38": "6775efafc9b33338", "39": "8176a164778526f9", "40": "66b69189c0acc3ff", "41": "0322ff824966a4d8", "42": "784c9e3d336a7f53", "43": "4b8bb6c3f7b64856", "44": "18c0ff3e6225bcb2", "45": "70b30e0a139bbd59", "46": "58f3596265f5f902", "47": "f89e819a4cc2d299", "48": "b45478cb1443df68", "49": "0d0f9f015921feef", "50": "8d0c81e5ca23e9a6", "51": "f79963571b9c15ee", "52": "5935824c825606cb", "53": "9685f9cbe158e50b", "54": "3d5ab759f350bc79", "55": "d549f24931a990e9", "56": "3cc031dcb648797c", "57": "a0ab88e6c629251d", "58": "76bd6412e2a22ecd", "59": "54827845564490c9", "60": "0a9736c416c0c6b9", "61": "77358620ac528153", "62": "3b0c585df09df48a", "63": "7e78cd3b28828c20", "64": "1c0c958231735f61", "65": "60b0f81225f62f69", "66": "920754c65cc94b88", "67": "df911472140a9b94", "68": "8e17bc11bcea91b9", "69": "7e905b75e4f28b95", "70": "a28424eca5d36c55", "71": "2cdb53d5b6051ab6", "72": "e42fbd3dc744e730", "73": "7fe2cac980de160c", "74": "2b1343a6a9db1487", "75": "54a1bb232f1d4ceb", "76": "442ba11d31ec0e0a", "77": "852a25b8b95bf8d1", "78": "1810ab370b9cd608", "79": "0fc5dca02a3f02f6", "80": "6ff8a97e63c9a2f3", "81": "a38f84ae3d895236", "82": "08e48bd11f9714df", "83": "5095122914e83cf5", "84": "1aef305bd7d7d63f", "85": "f8bfd0cfe9e8b478", "86": "410d15a346bd5894", "87": "6b41d288cfd834ab", "88": "5aa6db96312a8811", "89": "80225792bb62ba04", "90": "fd291228c3311f40", "91": "d4579c5b7aa2742a", "92": "7b9ba7c3bff11361", "93": "71cd9c1fa4a857f0", "94": "34cd980be3c32d21", "95": "970093453f3b7d90", "96": "9e96780a2b7c4bd6", "97": "a4e57776e229c794", "98": "089ea6a55861c693", "99": "8968e9e7d55afcbe", "100": "daa6156e559cda08", "101": "14e5b6323d34c337", "102": "6aa00d5a83295f11", "103": "f58668f5b10ccdeb", "104": "4ec787414cc6f50b", "105": "de672873b6dec6e2", "106": "edcd5d9bb3c4b00f", "107": "37f2f47110fe3eaa", "108": "b7ad5abb1da8cf8d", "109": "cb48a9c4f54efa38", "110": "f6dd36fd7f3edbe5", "111": "2925b620f0b1fd17", "112": "d8b3099917c3b711", "113": "3bb61caa0302c804", "114": "0d4f1d08dd056bb9"} {"0": "36bdb6f09c457f7c", "1": "8c5bf6244cf710c6", "2": "efbcc9c62a3ee78b", "3": "8599153989b07faa", "4": "b5952a1f7fee9f20", "5": "5b8462a3f82d188c", "6": "80f79e9e2011a3e3", "7": "4654167fd211d027", "8": "50acfa00fe353440", "9": "c6d2f770737823f1", "10": "44f2ca451aea24be", "11": "feaac5ab67a8c17a", "12": "b71bd92e5edbf2e0", "13": "219d65ba6d2689e4", "14": "8e30bb8112fd02d1", "15": "03906ab80b99db85", "16": "5d51c60ba1bc2be0", "17": "a1da914f522dcd21", "18": "fbad840891b90569", "19": "0686ff2d6fe29fb3", "20": "060baa9e1924b465", "21": "a5c8f2c3080b8243", "22": "0d76852f1d29eeb1", "23": "6ff68619f2d72924", "24": "3da11675eee7ec46", "25": "a6696589e9556f97", "26": "6c752999e8a4d4b6", "27": "2d4e13ea2111d750", "28": "4b60cb0ee1ac186a", "29": "f56fbca9bb8235ec", "30": "c7beed940704509f", "31": "38be2d254fb31ae8", "32": "ee5596fcf7e7c0b3", "33": "e4d4e0a440bc599f", "34": "c897e49c001acdae", "35": "3aad272a2cf5d495", "36": "0a197439d306b956", "37": "f43acf5c8b1329af", "38": "6775efafc9b33338", "39": "8176a164778526f9", "40": "66b69189c0acc3ff", "41": "0322ff824966a4d8", "42": "784c9e3d336a7f53", "43": "4b8bb6c3f7b64856", "44": "18c0ff3e6225bcb2", "45": "722991c46c10afed", "46": "137ad23716b24a73", "47": "69c7215a1a600ded", "48": "0237e1e02ee47a27", "49": "0d0f9f015921feef", "50": "8d0c81e5ca23e9a6", "51": "f79963571b9c15ee", "52": "5935824c825606cb", "53": "9685f9cbe158e50b", "54": "3d5ab759f350bc79", "55": "d549f24931a990e9", "56": "3cc031dcb648797c", "57": "a0ab88e6c629251d", "58": "76bd6412e2a22ecd", "59": "54827845564490c9", "60": "0a9736c416c0c6b9", "61": "77358620ac528153", "62": "3b0c585df09df48a", "63": "7e78cd3b28828c20", "64": "1c0c958231735f61", "65": "60b0f81225f62f69", "66": "920754c65cc94b88", "67": "df911472140a9b94", "68": "8e17bc11bcea91b9", "69": "7e905b75e4f28b95", "70": "a28424eca5d36c55", "71": "2cdb53d5b6051ab6", "72": "e42fbd3dc744e730", "73": "7fe2cac980de160c", "74": "2b1343a6a9db1487", "75": "54a1bb232f1d4ceb", "76": "442ba11d31ec0e0a", "77": "852a25b8b95bf8d1", "78": "1810ab370b9cd608", "79": "0fc5dca02a3f02f6", "80": "6ff8a97e63c9a2f3", "81": "a38f84ae3d895236", "82": "08e48bd11f9714df", "83": "5095122914e83cf5", "84": "1aef305bd7d7d63f", "85": "f8bfd0cfe9e8b478", "86": "410d15a346bd5894", "87": "6b41d288cfd834ab", "88": "5aa6db96312a8811", "89": "80225792bb62ba04", "90": "fd291228c3311f40", "91": "d4579c5b7aa2742a", "92": "7b9ba7c3bff11361", "93": "71cd9c1fa4a857f0", "94": "34cd980be3c32d21", "95": "970093453f3b7d90", "96": "9e96780a2b7c4bd6", "97": "b7c10b0e09caac0b", "98": "089ea6a55861c693", "99": "cb6165a7dc822d29", "100": "94ef9e0e5a443fe6", "101": "f1ad5eee3650a7ca", "102": "6aa00d5a83295f11", "103": "f58668f5b10ccdeb", "104": "4ec787414cc6f50b", "105": "1cf3077fd45d874a", "106": "edcd5d9bb3c4b00f", "107": "37f2f47110fe3eaa", "108": "b7ad5abb1da8cf8d", "109": "cb48a9c4f54efa38", "110": "f6dd36fd7f3edbe5", "111": "2925b620f0b1fd17", "112": "d8b3099917c3b711", "113": "3bb61caa0302c804", "114": "0d4f1d08dd056bb9", "115": "4ac2dcddeec2ff11", "116": "07da9aae9668f573", "117": "196f63e0c4536d30", "118": "ade84262e3cfac12", "119": "ebe4e5e0c42c613f", "120": "27256931b19a2867", "121": "65c28abd5c9a3103", "122": "aa8a1de55696b666", "123": "96618c9a362af46c", "124": "83f104cbb62fd03e", "125": "6db738fb27190349", "126": "6a087a22cbcef972", "127": "85fd71a0cad8d3a5", "128": "22dd4feed96c4229", "129": "c4d2f60f532e6f16", "130": "f6b0aa8a1568926b", "131": "142d0db70bad18fe", "132": "67ea4284cbc02c54", "133": "d899cfc86c7a4a27", "134": "ba9464410a9b4168", "135": "0d496a12149eca27", "136": "521f5c7b9d566b4d", "137": "9e37828bdd2ba8c5", "138": "ec03c97194c56f91", "139": "f4e6d5dfa30034c5", "140": "d9b47fa423cf0748", "141": "1e0330b8757f333e", "142": "f35d75e1194c008d", "143": "4d2ae7190b514a34", "144": "4a98716cabf43f86", "145": "56b2431193739c38", "146": "edc785fd71bb0675", "147": "4c7347f8f86e1fbd", "148": "8ca77cc4fd6fd437", "149": "8968e9e7d55afcbe", "150": "f4f4ce1a1180ddb1", "151": "e426746f6e9ee15f", "152": "290e66456e1dbc21", "153": "a3593e6f45bafb20", "154": "56747bad6345d66b", "155": "a8e7498fa7e257df", "156": "56e7b2355898077f", "157": "f2fc88f7d8214711", "158": "c966f6f8570c8c29", "159": "5f6094aa385f3bfe", "160": "2834e7d59672e756", "161": "cc6e436d94fd0033", "162": "64f33a2fc8969cd2", "163": "26ac1c0a00eabce1", "164": "cdcea44a6805ae55", "165": "b93c2f21e3c60614", "166": "85c97dab928b9b1b", "167": "7ab5695391e32126", "168": "4ac0ca3e4db3991c", "169": "a68dbc0869da4c5e", "170": "0fff6a9dc7ef908d", "171": "c93e305384cf368f", "172": "b9b5ff435e14f765", "173": "cbb280810ad58329"}
+21 -5
View File
@@ -44,10 +44,10 @@
"42": "1. Input Schemas", "42": "1. Input Schemas",
"43": "2. Basic CLI Usage Examples", "43": "2. Basic CLI Usage Examples",
"44": "2. Standard Streams & Exit Codes", "44": "2. Standard Streams & Exit Codes",
"45": "LocalEmbeddingsAdapter", "45": "ClassificationResult",
"46": "InherenceClassifier", "46": "InherenceClassifier",
"47": "detect_language", "47": "detect_language",
"48": "classifier.py", "48": "models.py",
"49": "content_northvolt_de.md", "49": "content_northvolt_de.md",
"50": "content_presal_pt.md", "50": "content_presal_pt.md",
"51": "content_tangential_es.md", "51": "content_tangential_es.md",
@@ -99,12 +99,12 @@
"97": "🧠 TextNLPClassifierApp", "97": "🧠 TextNLPClassifierApp",
"98": "Extraction Pipeline Checklist: Article Content Multi-Engine Extractor", "98": "Extraction Pipeline Checklist: Article Content Multi-Engine Extractor",
"99": "sample_rss_xml", "99": "sample_rss_xml",
"100": "models.py", "100": "ClassificationError",
"101": "ECPSnapshot", "101": "ECPSnapshot",
"102": "Feature Specification: Multilingual NLP Entity Inherence Classifier (POC)", "102": "Feature Specification: Multilingual NLP Entity Inherence Classifier (POC)",
"103": "4. Requisitos Funcionais (FR)", "103": "4. Requisitos Funcionais (FR)",
"104": "Tasks: Article Content Multi-Engine Extractor", "104": "Tasks: Article Content Multi-Engine Extractor",
"105": "ClassificationResult", "105": "classifier.py",
"106": "Implementation Plan: Article Content Multi-Engine Extractor", "106": "Implementation Plan: Article Content Multi-Engine Extractor",
"107": "2. Cenários de Validação", "107": "2. Cenários de Validação",
"108": "1. Technical Decisions & Tradeoffs", "108": "1. Technical Decisions & Tradeoffs",
@@ -113,5 +113,21 @@
"111": "Specification Quality Checklist: Multilingual NLP Entity Inherence Classifier", "111": "Specification Quality Checklist: Multilingual NLP Entity Inherence Classifier",
"112": "CLI Contract: Article Content Multi-Engine Extractor", "112": "CLI Contract: Article Content Multi-Engine Extractor",
"113": "001-multilingual-entity-classifier/spec.md", "113": "001-multilingual-entity-classifier/spec.md",
"114": "JSON Schema Contract: Article Content Multi-Engine Extractor" "114": "JSON Schema Contract: Article Content Multi-Engine Extractor",
"115": "PRD — Seleção determinística da biblioteca de extração de conteúdo",
"116": "select_article_extractor.py",
"117": "1. Text Normalization Pipeline",
"118": "Tasks: Deterministic Article Content Selection",
"119": "select_article_extractor",
"120": "process_batch",
"122": "test_select_article_extractor.py",
"123": "Feature Specification: Deterministic Content Selection",
"124": "2. Entity Descriptions & Fields",
"125": "Implementation Plan: Deterministic Article Content Selection",
"126": "Deterministic Content Selection Checklist: End-to-End Requirements Quality",
"127": "Quickstart: Deterministic Article Content Selection",
"128": "Specification Quality Checklist: Deterministic Content Selection",
"129": "CLI Interface Contract: Deterministic Article Content Selection",
"130": "004-deterministic-content-selection/spec.md",
"131": "JSON Schema Contract: Deterministic Article Content Selection"
} }
+120 -45
View File
@@ -1,16 +1,16 @@
# Graph Report - TextNLPClassifierApp (2026-08-20) # Graph Report - TextNLPClassifierApp (2026-08-20)
## Corpus Check ## Corpus Check
- 161 files · ~78,476 words - 174 files · ~91,254 words
- Verdict: corpus is large enough that graph structure adds value. - Verdict: corpus is large enough that graph structure adds value.
## Summary ## Summary
- 1000 nodes · 1226 edges · 115 communities (77 shown, 38 thin omitted) - 1233 nodes · 1546 edges · 131 communities (93 shown, 38 thin omitted)
- Extraction: 97% EXTRACTED · 3% INFERRED · 0% AMBIGUOUS · INFERRED: 39 edges (avg confidence: 0.95) - Extraction: 97% EXTRACTED · 3% INFERRED · 0% AMBIGUOUS · INFERRED: 51 edges (avg confidence: 0.95)
- Token cost: 0 input · 0 output - Token cost: 0 input · 0 output
## Graph Freshness ## Graph Freshness
- Built from commit: `6e3d5761` - Built from commit: `6a45368c`
- Run `git rev-parse HEAD` and compare to check if the graph is stale. - Run `git rev-parse HEAD` and compare to check if the graph is stale.
- Run `graphify update .` after code changes (no API cost). - Run `graphify update .` after code changes (no API cost).
@@ -56,10 +56,10 @@
- 1. Input Schemas - 1. Input Schemas
- 2. Basic CLI Usage Examples - 2. Basic CLI Usage Examples
- 2. Standard Streams & Exit Codes - 2. Standard Streams & Exit Codes
- LocalEmbeddingsAdapter - ClassificationResult
- InherenceClassifier - InherenceClassifier
- detect_language - detect_language
- classifier.py - models.py
- content_northvolt_de.md - content_northvolt_de.md
- content_presal_pt.md - content_presal_pt.md
- content_tangential_es.md - content_tangential_es.md
@@ -110,12 +110,12 @@
- 🧠 TextNLPClassifierApp - 🧠 TextNLPClassifierApp
- Extraction Pipeline Checklist: Article Content Multi-Engine Extractor - Extraction Pipeline Checklist: Article Content Multi-Engine Extractor
- sample_rss_xml - sample_rss_xml
- models.py - ClassificationError
- ECPSnapshot - ECPSnapshot
- Feature Specification: Multilingual NLP Entity Inherence Classifier (POC) - Feature Specification: Multilingual NLP Entity Inherence Classifier (POC)
- 4. Requisitos Funcionais (FR) - 4. Requisitos Funcionais (FR)
- Tasks: Article Content Multi-Engine Extractor - Tasks: Article Content Multi-Engine Extractor
- ClassificationResult - classifier.py
- Implementation Plan: Article Content Multi-Engine Extractor - Implementation Plan: Article Content Multi-Engine Extractor
- 2. Cenários de Validação - 2. Cenários de Validação
- 1. Technical Decisions & Tradeoffs - 1. Technical Decisions & Tradeoffs
@@ -124,18 +124,33 @@
- Specification Quality Checklist: Multilingual NLP Entity Inherence Classifier - Specification Quality Checklist: Multilingual NLP Entity Inherence Classifier
- CLI Contract: Article Content Multi-Engine Extractor - CLI Contract: Article Content Multi-Engine Extractor
- JSON Schema Contract: Article Content Multi-Engine Extractor - JSON Schema Contract: Article Content Multi-Engine Extractor
- PRD — Seleção determinística da biblioteca de extração de conteúdo
- select_article_extractor.py
- 1. Text Normalization Pipeline
- Tasks: Deterministic Article Content Selection
- select_article_extractor
- process_batch
- test_select_article_extractor.py
- Feature Specification: Deterministic Content Selection
- 2. Entity Descriptions & Fields
- Implementation Plan: Deterministic Article Content Selection
- Deterministic Content Selection Checklist: End-to-End Requirements Quality
- Quickstart: Deterministic Article Content Selection
- Specification Quality Checklist: Deterministic Content Selection
- CLI Interface Contract: Deterministic Article Content Selection
- JSON Schema Contract: Deterministic Article Content Selection
## God Nodes (most connected - your core abstractions) ## God Nodes (most connected - your core abstractions)
1. `ECPSnapshot` - 31 edges 1. `ECPSnapshot` - 31 edges
2. `InherenceClassifier` - 25 edges 2. `InherenceClassifier` - 25 edges
3. `DecisionCategory` - 18 edges 3. `select_article_extractor()` - 23 edges
4. `ClassificationResult` - 17 edges 4. `ExtractorName` - 21 edges
5. `process_batch()` - 15 edges 5. `DecisionCategory` - 17 edges
6. `LocalEmbeddingsAdapter` - 14 edges 6. `ClassificationResult` - 17 edges
7. `LLMFallbackAdapter` - 14 edges 7. `process_batch()` - 15 edges
8. `detect_language()` - 14 edges 8. `process_batch()` - 14 edges
9. `main()` - 13 edges 9. `LocalEmbeddingsAdapter` - 14 edges
10. `ArticleCrawler` - 13 edges 10. `LLMFallbackAdapter` - 14 edges
## Surprising Connections (you probably didn't know these) ## Surprising Connections (you probably didn't know these)
- `main()` --uses--> `ECPSnapshot` [INFERRED] - `main()` --uses--> `ECPSnapshot` [INFERRED]
@@ -146,13 +161,13 @@
tests/test_benchmark_24.py → src/classifier.py tests/test_benchmark_24.py → src/classifier.py
- `test_classification_result_serialization()` --uses--> `DecisionCategory` [INFERRED] - `test_classification_result_serialization()` --uses--> `DecisionCategory` [INFERRED]
tests/test_models.py → src/models.py tests/test_models.py → src/models.py
- `petrobras_ecp()` --uses--> `ECPSnapshot` [INFERRED] - `test_classification_error_serialization()` --uses--> `ErrorCode` [INFERRED]
tests/test_classifier.py → src/models.py tests/test_models.py → src/models.py
## Import Cycles ## Import Cycles
- None detected. - None detected.
## Communities (115 total, 38 thin omitted) ## Communities (131 total, 38 thin omitted)
### Community 0 - "Task Planning" ### Community 0 - "Task Planning"
Cohesion: 0.07 Cohesion: 0.07
@@ -294,21 +309,21 @@ Nodes (7): 1. Prerequisites & Installation, 2.1 Direct Inherence (Portuguese), 2
Cohesion: 0.29 Cohesion: 0.29
Nodes (6): 1.1 Arguments & Options, 1. Command Line Interface, 2.1 Exit Codes, 2.2 Standard Output (`stdout`) / Standard Error (`stderr`), 2. Standard Streams & Exit Codes, CLI Contract & Interface Specification (POC) Nodes (6): 1.1 Arguments & Options, 1. Command Line Interface, 2.1 Exit Codes, 2.2 Standard Output (`stdout`) / Standard Error (`stderr`), 2. Standard Streams & Exit Codes, CLI Contract & Interface Specification (POC)
### Community 45 - "LocalEmbeddingsAdapter" ### Community 45 - "ClassificationResult"
Cohesion: 0.14 Cohesion: 0.17
Nodes (8): LocalEmbeddingsAdapter, Optional adapter for local multilingual semantic vector embeddings., LLMFallbackAdapter, Optional adapter for LLM fallback boundary disambiguation., Unit tests for optional adapter interfaces (Tier 2 / Tier 3)., test_classifier_with_adapter_flags(), test_embeddings_adapter_interface(), test_llm_adapter_interface() Nodes (10): ABC, BaseNLPAdapter, Base abstract adapter interface for optional Tier 2 / Tier 3 NLP enhancers., Abstract interface for pluggable NLP classification adapters., Return True if the underlying provider or model is installed and configured., Compute semantic similarity score between text and a set of candidate terms., Optionally refine an ambiguous classification result., Optional LLM fallback adapter (Tier 3). Disabled by default. Provides fallback… (+2 more)
### Community 46 - "InherenceClassifier" ### Community 46 - "InherenceClassifier"
Cohesion: 0.12 Cohesion: 0.12
Nodes (27): InherenceClassifier, Tier 1 Deterministic NLP Entity Inherence Classifier., DecisionCategory, RelatedEntity, Adversarial and robustness test suite for Multilingual NLP Entity Inherence…, Run CLI via subprocess with empty content and verify error code and exit code., Content about city/state governance of São Paulo against ECP for São Paulo FC., Run CLI via subprocess with missing target_name and verify error payload. (+19 more) Nodes (27): InherenceClassifier, Tier 1 Deterministic NLP Entity Inherence Classifier., DecisionCategory, RelatedEntity, Adversarial and robustness test suite for Multilingual NLP Entity Inherence…, Run CLI via subprocess without --output and verify stdout is pure parseable…, Run CLI via subprocess with empty content and verify error code and exit code., Content about city/state governance of São Paulo against ECP for São Paulo FC. (+19 more)
### Community 47 - "detect_language" ### Community 47 - "detect_language"
Cohesion: 0.14 Cohesion: 0.09
Nodes (21): count_phrase_occurrences(), match_phrase_in_text(), Check if a normalized phrase appears in normalized text with word boundary…, Count occurrences of a phrase in text., Classify inherence of content against an ECP snapshot., detect_language(), extract_words(), normalize_text() (+13 more) Nodes (30): count_phrase_occurrences(), match_phrase_in_text(), Check if a normalized phrase appears in normalized text with word boundary…, Count occurrences of a phrase in text., Classify inherence of content against an ECP snapshot., detect_language(), extract_words(), normalize_text() (+22 more)
### Community 48 - "classifier.py" ### Community 48 - "models.py"
Cohesion: 0.24 Cohesion: 0.19
Nodes (11): Core deterministic classification engine (Tier 1 core)., extract_evidence_snippets(), extract_sentences(), Markdown content parser and excerpt extraction utilities., Remove markdown syntax markers (headers, bold, italics, links, code blocks) to…, Split text into individual sentences., Extract relevant sentence excerpts from Markdown text that contain any of the…, strip_markdown() (+3 more) Nodes (15): emit_error(), main(), parse_args(), Namespace, ErrorCode, MatchedGraphEntity, Enum, str (+7 more)
### Community 80 - "test_extract_article_contents.py" ### Community 80 - "test_extract_article_contents.py"
Cohesion: 0.06 Cohesion: 0.06
@@ -382,13 +397,13 @@ Nodes (34): 1. Requirement Completeness, 2. Requirement Clarity & Non-Ambiguity,
Cohesion: 0.67 Cohesion: 0.67
Nodes (3): fixture, Fixture que fornece o conteúdo do XML de exemplo para testes offline., sample_rss_xml() Nodes (3): fixture, Fixture que fornece o conteúdo do XML de exemplo para testes offline., sample_rss_xml()
### Community 100 - "models.py" ### Community 100 - "ClassificationError"
Cohesion: 0.15 Cohesion: 0.29
Nodes (17): emit_error(), main(), parse_args(), Namespace, Enum, ClassificationError, ErrorCode, MatchedGraphEntity (+9 more) Nodes (3): ClassificationError, Any, test_classification_error_serialization()
### Community 101 - "ECPSnapshot" ### Community 101 - "ECPSnapshot"
Cohesion: 0.22 Cohesion: 0.24
Nodes (10): parametrize, ECPSnapshot, Any, classifier(), fixture, Controlled 24-case benchmark suite for Multilingual NLP Entity Inherence…, test_benchmark_case(), test_ecp_snapshot_defaults() (+2 more) Nodes (10): parametrize, ECPSnapshot, classifier(), fixture, Controlled 24-case benchmark suite for Multilingual NLP Entity Inherence…, test_benchmark_case(), Unit tests for ECP models, schema validation, and structured error handling., test_ecp_snapshot_defaults() (+2 more)
### Community 102 - "Feature Specification: Multilingual NLP Entity Inherence Classifier (POC)" ### Community 102 - "Feature Specification: Multilingual NLP Entity Inherence Classifier (POC)"
Cohesion: 0.14 Cohesion: 0.14
@@ -402,13 +417,13 @@ Nodes (20): 1.1 Objetivo do Produto, 1. Visão Geral e Contexto, 2. Personas e C
Cohesion: 0.11 Cohesion: 0.11
Nodes (18): Dependencies & Execution Order, Entrega Incremental, Implementation Strategy, Implementação da User Story 1, Implementação da User Story 2, Implementação da User Story 3, MVP First (User Story 1 Only), Oportunidades de Execução Paralela (+10 more) Nodes (18): Dependencies & Execution Order, Entrega Incremental, Implementation Strategy, Implementação da User Story 1, Implementação da User Story 2, Implementação da User Story 3, MVP First (User Story 1 Only), Oportunidades de Execução Paralela (+10 more)
### Community 105 - "ClassificationResult" ### Community 105 - "classifier.py"
Cohesion: 0.16 Cohesion: 0.13
Nodes (11): ABC, BaseNLPAdapter, Base abstract adapter interface for optional Tier 2 / Tier 3 NLP enhancers., Abstract interface for pluggable NLP classification adapters., Return True if the underlying provider or model is installed and configured., Compute semantic similarity score between text and a set of candidate terms., Optionally refine an ambiguous classification result., Optional local vector embeddings adapter (Tier 2). Disabled by default.… (+3 more) Nodes (10): LocalEmbeddingsAdapter, Optional local vector embeddings adapter (Tier 2). Disabled by default.…, Optional adapter for local multilingual semantic vector embeddings., LLMFallbackAdapter, Optional adapter for LLM fallback boundary disambiguation., Core deterministic classification engine (Tier 1 core)., Unit tests for optional adapter interfaces (Tier 2 / Tier 3)., test_classifier_with_adapter_flags() (+2 more)
### Community 106 - "Implementation Plan: Article Content Multi-Engine Extractor" ### Community 106 - "Implementation Plan: Article Content Multi-Engine Extractor"
Cohesion: 0.17 Cohesion: 0.17
Nodes (11): Constitution Check, Documentation (this feature), Implementation Phases, Implementation Plan: Article Content Multi-Engine Extractor, Phase 0: Outline & Research *(Completed)*, Phase 1: Design & Contracts *(Completed)*, Phase 2: Tasks & Execution *(Next: `/speckit-tasks`)*, Project Structure (+3 more) Nodes (11): Constitution Check, Documentation (this feature), Implementation Phases, Implementation Plan: Article Content Multi-Engine Extractor, Phase 0: Outline & Research *(Completed)*, Phase 1: Design & Contracts *(Completed)*, Phase 2: Tasks & Execution *(Completed)*, Project Structure (+3 more)
### Community 107 - "2. Cenários de Validação" ### Community 107 - "2. Cenários de Validação"
Cohesion: 0.22 Cohesion: 0.22
@@ -438,25 +453,85 @@ Nodes (5): 1. Comando de Execução, 2. Argumentos e Flags, 3. Códigos de Saíd
Cohesion: 0.50 Cohesion: 0.50
Nodes (3): 1. Schema de Entrada (Input JSON), 2. Schema de Saída (Output JSON), JSON Schema Contract: Article Content Multi-Engine Extractor Nodes (3): 1. Schema de Entrada (Input JSON), 2. Schema de Saída (Output JSON), JSON Schema Contract: Article Content Multi-Engine Extractor
### Community 115 - "PRD — Seleção determinística da biblioteca de extração de conteúdo"
Cohesion: 0.07
Nodes (29): 10. Requisitos não funcionais, 11. Critérios de aceite, 12. Casos obrigatórios de teste, 13. Definition of Done, 1. Contexto, 2. Objetivo, 3.1 Incluído, 3.2 Fora do escopo (+21 more)
### Community 116 - "select_article_extractor.py"
Cohesion: 0.16
Nodes (17): BatchProcessingResult, break_priority_tie(), calculate_consensus_metrics(), ExtractorCandidate, form_active_set(), main(), parse_args(), Namespace (+9 more)
### Community 117 - "1. Text Normalization Pipeline"
Cohesion: 0.11
Nodes (18): 1. Text Normalization Pipeline, 2. 5-Token Shingles & Consensus Metrics, 3. Regras de Decisão, Empate Técnico e Desempate Hierárquico, 4. Estratégia de I/O Não Destrutiva e Escrita Atômica, Alternatives Considered, Context, Context, Context (+10 more)
### Community 118 - "Tasks: Deterministic Article Content Selection"
Cohesion: 0.11
Nodes (18): Dependencies & Execution Order, Implementation for User Story 1, Implementation for User Story 2, Implementation for User Story 3, Implementation Strategy, Incremental Delivery (Phases 4, 5 & 6), MVP First (Phases 1, 2 & 3), Parallel Opportunities (+10 more)
### Community 119 - "select_article_extractor"
Cohesion: 0.08
Nodes (37): ArticleSelectionResult, CandidateStatus, extract_candidate_data(), ExtractorName, Any, Enum, str, Extrai campo de texto, erro e calcula tokens/shingles para um motor. (+29 more)
### Community 120 - "process_batch"
Cohesion: 0.10
Nodes (26): atomic_save_json(), process_batch(), Path, Salva dados em JSON de forma atômica utilizando arquivo temporário e rename., Lê o JSON de entrada, valida a estrutura, processa todos os artigos e grava o…, Path, CT-012: A entrada já contém selected_extractor -> Recalcular e substituir…, CT-013: articles está vazio -> Gerar saída válida com articles vazio. (+18 more)
### Community 122 - "test_select_article_extractor.py"
Cohesion: 0.21
Nodes (14): generate_shingles(), normalize_text(), Executa a normalização determinística para comparação: 1. Decodificar entidades…, Gera conjunto de shingles ordenados de tamanho window_size (padrão 5). - Se…, Suíte de Testes Automatizados para o Seletor Determinístico de Extrator. Cobre…, Garante que marcação de imagem Markdown ![alt](url) seja descartada e link…, test_generate_shingles_empty(), test_generate_shingles_short_text() (+6 more)
### Community 123 - "Feature Specification: Deterministic Content Selection"
Cohesion: 0.17
Nodes (12): Assumptions, Edge Cases, Feature Specification: Deterministic Content Selection, Functional Requirements, Key Entities *(include if feature involves data)*, Measurable Outcomes, Requirements *(mandatory)*, Success Criteria *(mandatory)* (+4 more)
### Community 124 - "2. Entity Descriptions & Fields"
Cohesion: 0.18
Nodes (11): 1. Domain Entities & Value Types, 2. Entity Descriptions & Fields, 3. JSON Schema Mapping, `ArticleSelectionResult` (Dataclass), `BatchProcessingResult` (Dataclass), `CandidateStatus` (Enum), Data Model: Deterministic Content Selection, Entrada (+3 more)
### Community 125 - "Implementation Plan: Deterministic Article Content Selection"
Cohesion: 0.18
Nodes (11): Constitution Check, Documentation (this feature), Implementation Phases, Implementation Plan: Deterministic Article Content Selection, Phase 0: Outline & Research *(Completed)*, Phase 1: Design & Contracts *(Completed)*, Phase 2: Tasks & Execution *(Next Step via `/speckit-tasks`)*, Project Structure (+3 more)
### Community 126 - "Deterministic Content Selection Checklist: End-to-End Requirements Quality"
Cohesion: 0.25
Nodes (7): Candidate State Transitions & Resilience, Decision & Tie-Breaking Hierarchy, Deterministic Content Selection Checklist: End-to-End Requirements Quality, JSON Schema Integrity & Atomic I/O, Notes, Shingles & Consensus Metric Formulation, Text Normalization & Tokenization Quality
### Community 127 - "Quickstart: Deterministic Article Content Selection"
Cohesion: 0.29
Nodes (7): 1. Pré-requisitos, 2. Execução Rápida via CLI, 3. Execução dos Testes Automatizados, 4. Validação Programática / Uso como Módulo Python, Cenário 1: Selecionar o melhor extrator para uma extração existente, Cenário 2: Especificar caminho de saída customizado e modo verboso, Quickstart: Deterministic Article Content Selection
### Community 128 - "Specification Quality Checklist: Deterministic Content Selection"
Cohesion: 0.33
Nodes (5): Content Quality, Feature Readiness, Notes, Requirement Completeness, Specification Quality Checklist: Deterministic Content Selection
### Community 129 - "CLI Interface Contract: Deterministic Article Content Selection"
Cohesion: 0.33
Nodes (5): 1. Command Syntax, 2. Arguments and Flags, 3. Standard Streams (I/O), 4. Exit Codes, CLI Interface Contract: Deterministic Article Content Selection
### Community 131 - "JSON Schema Contract: Deterministic Article Content Selection"
Cohesion: 0.50
Nodes (3): 1. Input JSON Schema, 2. Output JSON Schema, JSON Schema Contract: Deterministic Article Content Selection
## Knowledge Gaps ## Knowledge Gaps
- **465 isolated node(s):** `text-nlp-classifier`, `MatchedGraphEntity`, `graphify`, `Usage`, `What graphify is for` (+460 more) - **560 isolated node(s):** `text-nlp-classifier`, `MatchedGraphEntity`, `graphify`, `Usage`, `What graphify is for` (+555 more)
These have ≤1 connection - possible missing edges or undocumented components. These have ≤1 connection - possible missing edges or undocumented components.
- **38 thin communities (<3 nodes) omitted from report** — run `graphify query` to explore isolated nodes. - **38 thin communities (<3 nodes) omitted from report** — run `graphify query` to explore isolated nodes.
## Suggested Questions ## Suggested Questions
_Questions this graph is uniquely positioned to answer:_ _Questions this graph is uniquely positioned to answer:_
- **Why does `ECPSnapshot` connect `ECPSnapshot` to `models.py`, `ClassificationResult`, `LocalEmbeddingsAdapter`, `InherenceClassifier`, `detect_language`, `classifier.py`?** - **Why does `ECPSnapshot` connect `ECPSnapshot` to `classifier.py`, `ClassificationResult`, `InherenceClassifier`, `detect_language`, `models.py`?**
_High betweenness centrality (0.005) - this node is a cross-community bridge._
- **Why does `InherenceClassifier` connect `InherenceClassifier` to `models.py`, `ECPSnapshot`, `ClassificationResult`, `LocalEmbeddingsAdapter`, `detect_language`, `classifier.py`?**
_High betweenness centrality (0.003) - this node is a cross-community bridge._ _High betweenness centrality (0.003) - this node is a cross-community bridge._
- **Why does `detect_language()` connect `detect_language` to `classifier.py`?** - **Why does `LLMFallbackAdapter` connect `classifier.py` to `ECPSnapshot`, `ClassificationResult`, `InherenceClassifier`?**
_High betweenness centrality (0.003) - this node is a cross-community bridge._
- **Why does `Research & Architectural Decisions: Deterministic Content Selection` connect `1. Text Normalization Pipeline` to `004-deterministic-content-selection/spec.md`?**
_High betweenness centrality (0.003) - this node is a cross-community bridge._ _High betweenness centrality (0.003) - this node is a cross-community bridge._
- **Are the 10 inferred relationships involving `ECPSnapshot` (e.g. with `main()` and `BaseNLPAdapter`) actually correct?** - **Are the 10 inferred relationships involving `ECPSnapshot` (e.g. with `main()` and `BaseNLPAdapter`) actually correct?**
_`ECPSnapshot` has 10 INFERRED edges - model-reasoned connections that need verification._ _`ECPSnapshot` has 10 INFERRED edges - model-reasoned connections that need verification._
- **Are the 6 inferred relationships involving `InherenceClassifier` (e.g. with `LocalEmbeddingsAdapter` and `LLMFallbackAdapter`) actually correct?** - **Are the 6 inferred relationships involving `InherenceClassifier` (e.g. with `LocalEmbeddingsAdapter` and `LLMFallbackAdapter`) actually correct?**
_`InherenceClassifier` has 6 INFERRED edges - model-reasoned connections that need verification._ _`InherenceClassifier` has 6 INFERRED edges - model-reasoned connections that need verification._
- **Are the 12 inferred relationships involving `ExtractorName` (e.g. with `test_article_1_regression_technical_tie_markdown_images()` and `test_ct_001_three_candidates_clear_winner()`) actually correct?**
_`ExtractorName` has 12 INFERRED edges - model-reasoned connections that need verification._
- **Are the 10 inferred relationships involving `DecisionCategory` (e.g. with `InherenceClassifier` and `test_adversarial_apple_fruit_recipe()`) actually correct?** - **Are the 10 inferred relationships involving `DecisionCategory` (e.g. with `InherenceClassifier` and `test_adversarial_apple_fruit_recipe()`) actually correct?**
_`DecisionCategory` has 10 INFERRED edges - model-reasoned connections that need verification._ _`DecisionCategory` has 10 INFERRED edges - model-reasoned connections that need verification._
- **Are the 4 inferred relationships involving `ClassificationResult` (e.g. with `BaseNLPAdapter` and `LocalEmbeddingsAdapter`) actually correct?**
_`ClassificationResult` has 4 INFERRED edges - model-reasoned connections that need verification._
File diff suppressed because it is too large Load Diff
+150 -72
View File
@@ -294,15 +294,15 @@
"semantic_hash": "" "semantic_hash": ""
}, },
"classify.py": { "classify.py": {
"mtime": 1787197110.3753626, "mtime": 1787264412.2457643,
"seen": 1787197159.9828603, "seen": 1787264519.0539427,
"ast_hash": "d09a35a5e25d42f6ecc537d5b67bef8e", "ast_hash": "e89fc4b64bd7606fc466d5338108705b",
"semantic_hash": "" "semantic_hash": ""
}, },
"pyproject.toml": { "pyproject.toml": {
"mtime": 1787234982.768605, "mtime": 1787264482.8509245,
"seen": 1787235020.6850271, "seen": 1787264519.0539448,
"ast_hash": "26f5f979658067bf447f375f044a8f21", "ast_hash": "409c1fc0d7bbda6830ccb71c6196cb2f",
"semantic_hash": "" "semantic_hash": ""
}, },
"src/__init__.py": { "src/__init__.py": {
@@ -318,45 +318,45 @@
"semantic_hash": "" "semantic_hash": ""
}, },
"src/adapters/base.py": { "src/adapters/base.py": {
"mtime": 1787196396.6199949, "mtime": 1787264412.2437606,
"seen": 1787196463.0953116, "seen": 1787264519.0542011,
"ast_hash": "40dc6e175c8708467748c1f42a0e064a", "ast_hash": "220635d5d74e73f58259069bf5207908",
"semantic_hash": "" "semantic_hash": ""
}, },
"src/adapters/embeddings.py": { "src/adapters/embeddings.py": {
"mtime": 1787196402.7484262, "mtime": 1787264437.92723,
"seen": 1787196463.0953145, "seen": 1787264519.0542023,
"ast_hash": "0444823bb05720d4c4ca1662a00b2c01", "ast_hash": "7beb0ecfb7482c1fd10422da99f69abe",
"semantic_hash": "" "semantic_hash": ""
}, },
"src/adapters/llm.py": { "src/adapters/llm.py": {
"mtime": 1787196408.180451, "mtime": 1787264412.2437606,
"seen": 1787196463.0953166, "seen": 1787264519.0542035,
"ast_hash": "3d95d98625df3fcd343f2fecb78707c5", "ast_hash": "ba2990328f5b25ac6cdfc3b57acc9d39",
"semantic_hash": "" "semantic_hash": ""
}, },
"src/classifier.py": { "src/classifier.py": {
"mtime": 1787197085.0250447, "mtime": 1787264459.052089,
"seen": 1787197159.985785, "seen": 1787264519.0542045,
"ast_hash": "a2e70f968109d213fdab0ae81ac819ff", "ast_hash": "b8fb440374ecd66bb8bf67c70c19ed1f",
"semantic_hash": "" "semantic_hash": ""
}, },
"src/language.py": { "src/language.py": {
"mtime": 1787196384.5545745, "mtime": 1787264437.9312274,
"seen": 1787196463.0953217, "seen": 1787264519.0542057,
"ast_hash": "985619013e58a7f55af2e955817f223c", "ast_hash": "cbaf52272ebb05372e34a05cd201c270",
"semantic_hash": "" "semantic_hash": ""
}, },
"src/models.py": { "src/models.py": {
"mtime": 1787195855.3266282, "mtime": 1787264437.9302285,
"seen": 1787196463.0953236, "seen": 1787264519.054207,
"ast_hash": "ce73bfe85eb54f907e2052c80636ecaf", "ast_hash": "16e604c14f7d66634dc9e2ed7959dd7c",
"semantic_hash": "" "semantic_hash": ""
}, },
"src/parser.py": { "src/parser.py": {
"mtime": 1787195873.9119804, "mtime": 1787264437.9312274,
"seen": 1787196463.0953257, "seen": 1787264519.054208,
"ast_hash": "0a1d64ee7088b266125af071f85f3826", "ast_hash": "d43fca3f063534e3626f86f36a97a2cc",
"semantic_hash": "" "semantic_hash": ""
}, },
"tests/__init__.py": { "tests/__init__.py": {
@@ -366,39 +366,39 @@
"semantic_hash": "" "semantic_hash": ""
}, },
"tests/test_adapters.py": { "tests/test_adapters.py": {
"mtime": 1787196418.6328156, "mtime": 1787264437.9292288,
"seen": 1787196463.09533, "seen": 1787264519.0543096,
"ast_hash": "5caf95327ea030dbcc37a99bd9052ebd", "ast_hash": "7ed9ba6f62fef404bfc28a2e16ada647",
"semantic_hash": "" "semantic_hash": ""
}, },
"tests/test_benchmark_24.py": { "tests/test_benchmark_24.py": {
"mtime": 1787196325.5409796, "mtime": 1787264437.9302285,
"seen": 1787196463.0953324, "seen": 1787264519.0543122,
"ast_hash": "487d30d6de2dee529fa91b86a1d62c0a", "ast_hash": "052a51561002be035de1a79455256b67",
"semantic_hash": "" "semantic_hash": ""
}, },
"tests/test_classifier.py": { "tests/test_classifier.py": {
"mtime": 1787196364.7195728, "mtime": 1787264437.92723,
"seen": 1787196463.0953343, "seen": 1787264519.0543132,
"ast_hash": "4350a5b11a08d12ccb85a3f332d71e87", "ast_hash": "14da0c7a7d4c08762437ca777c086dea",
"semantic_hash": "" "semantic_hash": ""
}, },
"tests/test_cli.py": { "tests/test_cli.py": {
"mtime": 1787195939.567137, "mtime": 1787264437.9292288,
"seen": 1787196463.0953364, "seen": 1787264519.0543146,
"ast_hash": "90b0131d43cfc64a76e499c7a65d95de", "ast_hash": "b6f57994937363f8a1b0f2f66ea53d3a",
"semantic_hash": "" "semantic_hash": ""
}, },
"tests/test_language.py": { "tests/test_language.py": {
"mtime": 1787195891.3329668, "mtime": 1787264437.9292288,
"seen": 1787196463.0953388, "seen": 1787264519.0543184,
"ast_hash": "66191b43fcd7aef47c07cd20d5a8f03c", "ast_hash": "f1a09702410864434274f1b347769742",
"semantic_hash": "" "semantic_hash": ""
}, },
"tests/test_models.py": { "tests/test_models.py": {
"mtime": 1787195886.5009987, "mtime": 1787264437.9302285,
"seen": 1787196463.095341, "seen": 1787264519.0543194,
"ast_hash": "1fd725c8ca0f52e52f56c7223e9574d0", "ast_hash": "7ec72b612bf758be8c9a3617ce18b132",
"semantic_hash": "" "semantic_hash": ""
}, },
"examples/content_northvolt_de.md": { "examples/content_northvolt_de.md": {
@@ -570,27 +570,27 @@
"semantic_hash": "" "semantic_hash": ""
}, },
"tests/test_adversarial.py": { "tests/test_adversarial.py": {
"mtime": 1787197141.2340336, "mtime": 1787264437.9242287,
"seen": 1787197159.9881344, "seen": 1787264519.0543108,
"ast_hash": "5e8daf517ee32c261617d96264ef0473", "ast_hash": "d353633ce31a624a8dcda790efec4d5e",
"semantic_hash": "" "semantic_hash": ""
}, },
"scripts/extract_google_news.py": { "scripts/extract_google_news.py": {
"mtime": 1787236453.5231855, "mtime": 1787264437.92723,
"seen": 1787236509.1147037, "seen": 1787264519.0540311,
"ast_hash": "4216de490a87378d21dab707a3166679", "ast_hash": "97d8a193901d5e2edf6359fad256b7ef",
"semantic_hash": "" "semantic_hash": ""
}, },
"tests/test_extract_google_news.py": { "tests/test_extract_google_news.py": {
"mtime": 1787236301.513448, "mtime": 1787264437.9312274,
"seen": 1787236353.5582018, "seen": 1787264519.054317,
"ast_hash": "1fc9a108a8abfecf1d37c8642a885e9e", "ast_hash": "7561ad8fedcf1bc6cc8ed1e86a345532",
"semantic_hash": "" "semantic_hash": ""
}, },
"docs/googlenews_extractor_guia_completo.md": { "docs/googlenews_extractor_guia_completo.md": {
"mtime": 1787231605.0830677, "mtime": 1787264437.9282286,
"seen": 1787234772.8112028, "seen": 1787264519.0578,
"ast_hash": "59db8e652000ba6087a00289a0ce0959", "ast_hash": "d7c3c64e2009dded496a08c7c91f63b9",
"semantic_hash": "" "semantic_hash": ""
}, },
"specs/002-google-news-extractor/checklists/readiness.md": { "specs/002-google-news-extractor/checklists/readiness.md": {
@@ -654,21 +654,21 @@
"semantic_hash": "" "semantic_hash": ""
}, },
"README.md": { "README.md": {
"mtime": 1787240502.4633865, "mtime": 1787264334.4498444,
"seen": 1787240516.5990582, "seen": 1787264519.0577974,
"ast_hash": "a075f140e2dd8383bec3991f92b32b26", "ast_hash": "196db110cfde06179fb750fc75afb6fe",
"semantic_hash": "" "semantic_hash": ""
}, },
"scripts/extract_article_contents.py": { "scripts/extract_article_contents.py": {
"mtime": 1787241061.3097162, "mtime": 1787264465.3415215,
"seen": 1787241108.1811028, "seen": 1787264519.0540295,
"ast_hash": "08cd37738da1500b6912ffa1fb2fc0cc", "ast_hash": "2bea6b2a048526cb49d95fb88a001600",
"semantic_hash": "" "semantic_hash": ""
}, },
"tests/test_extract_article_contents.py": { "tests/test_extract_article_contents.py": {
"mtime": 1787240910.5980105, "mtime": 1787264437.9292288,
"seen": 1787241108.1826632, "seen": 1787264519.0543158,
"ast_hash": "98cf3b30fa6ce044d943eff381eaf8a7", "ast_hash": "56aee72480f05c714865791a935d539c",
"semantic_hash": "" "semantic_hash": ""
}, },
"docs/prd_extrator_artigos_nlp.md": { "docs/prd_extrator_artigos_nlp.md": {
@@ -708,9 +708,9 @@
"semantic_hash": "" "semantic_hash": ""
}, },
"specs/003-article-content-extractor/plan.md": { "specs/003-article-content-extractor/plan.md": {
"mtime": 1787239601.395844, "mtime": 1787264323.707266,
"seen": 1787240516.6008782, "seen": 1787264519.0605621,
"ast_hash": "406c3ebad1d32984c1b98f8a9b7ae9f1", "ast_hash": "b2232491829a303a49e749c75f3adca2",
"semantic_hash": "" "semantic_hash": ""
}, },
"specs/003-article-content-extractor/quickstart.md": { "specs/003-article-content-extractor/quickstart.md": {
@@ -726,9 +726,9 @@
"semantic_hash": "" "semantic_hash": ""
}, },
"specs/003-article-content-extractor/spec.md": { "specs/003-article-content-extractor/spec.md": {
"mtime": 1787239197.9064422, "mtime": 1787264319.2883234,
"seen": 1787240516.6008813, "seen": 1787264519.060745,
"ast_hash": "9fd196b7ba4d9ba7adb2cad25d094764", "ast_hash": "187b1307e28a02f0ac44895b5f9cca55",
"semantic_hash": "" "semantic_hash": ""
}, },
"specs/003-article-content-extractor/tasks.md": { "specs/003-article-content-extractor/tasks.md": {
@@ -736,5 +736,83 @@
"seen": 1787240516.6008823, "seen": 1787240516.6008823,
"ast_hash": "d95c4763728496703cd89590288a2bed", "ast_hash": "d95c4763728496703cd89590288a2bed",
"semantic_hash": "" "semantic_hash": ""
},
"scripts/select_article_extractor.py": {
"mtime": 1787273522.903319,
"seen": 1787273548.728257,
"ast_hash": "3e6938964c7e4549051cb1637207698d",
"semantic_hash": ""
},
"tests/test_select_article_extractor.py": {
"mtime": 1787274189.7777488,
"seen": 1787274207.7989414,
"ast_hash": "9f91e09d47f06140b243b6f516d14c60",
"semantic_hash": ""
},
"docs/prd_deterministic_content_selection.md": {
"mtime": 1787264674.2038286,
"seen": 1787272611.6604555,
"ast_hash": "2c421402df8b6469ce60eaa984dce666",
"semantic_hash": ""
},
"specs/004-deterministic-content-selection/checklists/deterministic-selection.md": {
"mtime": 1787272346.8692143,
"seen": 1787272611.6713927,
"ast_hash": "6d9bcda99dbd284ffcbce372102fd868",
"semantic_hash": ""
},
"specs/004-deterministic-content-selection/checklists/requirements.md": {
"mtime": 1787264728.7202728,
"seen": 1787272611.6713977,
"ast_hash": "074efe6ba551e93f7d005c5462805693",
"semantic_hash": ""
},
"specs/004-deterministic-content-selection/contracts/cli-contract.md": {
"mtime": 1787274329.2175446,
"seen": 1787274402.0944166,
"ast_hash": "2cd795eaed6198dc343424bd20673728",
"semantic_hash": ""
},
"specs/004-deterministic-content-selection/contracts/json-schema.md": {
"mtime": 1787271837.7328393,
"seen": 1787272611.6714034,
"ast_hash": "155f0bccb7bcffabcedb6ef86b8003c5",
"semantic_hash": ""
},
"specs/004-deterministic-content-selection/data-model.md": {
"mtime": 1787271825.5970395,
"seen": 1787272611.671406,
"ast_hash": "23ec4a144f4aeeed9fee259b6165bea3",
"semantic_hash": ""
},
"specs/004-deterministic-content-selection/plan.md": {
"mtime": 1787271850.1744206,
"seen": 1787272611.671409,
"ast_hash": "3bebc2a73a9632d42ccca646af592133",
"semantic_hash": ""
},
"specs/004-deterministic-content-selection/quickstart.md": {
"mtime": 1787274324.1467915,
"seen": 1787274402.0946271,
"ast_hash": "a7886ca618af43f33971213e2255b3c6",
"semantic_hash": ""
},
"specs/004-deterministic-content-selection/research.md": {
"mtime": 1787274316.8544433,
"seen": 1787274402.0946283,
"ast_hash": "73ac1637d31472e25b0a1709cd137e5c",
"semantic_hash": ""
},
"specs/004-deterministic-content-selection/spec.md": {
"mtime": 1787274311.1564865,
"seen": 1787274402.0946293,
"ast_hash": "b2c994a14871c2e6e1cf4a11db7dbeb5",
"semantic_hash": ""
},
"specs/004-deterministic-content-selection/tasks.md": {
"mtime": 1787272595.8186145,
"seen": 1787272611.6714194,
"ast_hash": "3a2617478f44c6400ed57088d2039f93",
"semantic_hash": ""
} }
} }
@@ -0,0 +1,516 @@
{
"communities": {
"0": [
"specify_templates_tasks_template",
"specify_templates_tasks_template_dependencies_execution_order",
"specify_templates_tasks_template_format_id_p_story_description",
"specify_templates_tasks_template_implementation_for_user_story_1",
"specify_templates_tasks_template_implementation_for_user_story_2",
"specify_templates_tasks_template_implementation_for_user_story_3",
"specify_templates_tasks_template_implementation_strategy",
"specify_templates_tasks_template_incremental_delivery",
"specify_templates_tasks_template_mvp_first_user_story_1_only",
"specify_templates_tasks_template_notes",
"specify_templates_tasks_template_parallel_example_user_story_1",
"specify_templates_tasks_template_parallel_opportunities",
"specify_templates_tasks_template_parallel_team_strategy",
"specify_templates_tasks_template_path_conventions",
"specify_templates_tasks_template_phase_1_setup_shared_infrastructure",
"specify_templates_tasks_template_phase_2_foundational_blocking_prerequisites",
"specify_templates_tasks_template_phase_3_user_story_1_title_priority_p1_mvp",
"specify_templates_tasks_template_phase_4_user_story_2_title_priority_p2",
"specify_templates_tasks_template_phase_5_user_story_3_title_priority_p3",
"specify_templates_tasks_template_phase_dependencies",
"specify_templates_tasks_template_phase_n_polish_cross_cutting_concerns",
"specify_templates_tasks_template_tasks_feature_name",
"specify_templates_tasks_template_tests_for_user_story_1_optional_only_if_tests_requested",
"specify_templates_tasks_template_tests_for_user_story_2_optional_only_if_tests_requested",
"specify_templates_tasks_template_tests_for_user_story_3_optional_only_if_tests_requested",
"specify_templates_tasks_template_user_story_dependencies",
"specify_templates_tasks_template_within_each_user_story"
],
"1": [
"agents_skills_speckit_converge_skill",
"agents_skills_speckit_converge_skill_1_initialize_convergence_context",
"agents_skills_speckit_converge_skill_2_load_artifacts_progressive_disclosure",
"agents_skills_speckit_converge_skill_3_build_the_intent_inventory",
"agents_skills_speckit_converge_skill_4_assess_the_codebase_and_classify_findings",
"agents_skills_speckit_converge_skill_5_assign_severity",
"agents_skills_speckit_converge_skill_6_present_the_in_session_findings_summary",
"agents_skills_speckit_converge_skill_7_append_convergence_tasks_or_report_converged",
"agents_skills_speckit_converge_skill_8_provide_next_actions_handoff",
"agents_skills_speckit_converge_skill_9_check_for_extension_hooks",
"agents_skills_speckit_converge_skill_convergence_findings",
"agents_skills_speckit_converge_skill_execution_steps",
"agents_skills_speckit_converge_skill_goal",
"agents_skills_speckit_converge_skill_operating_constraints",
"agents_skills_speckit_converge_skill_pre_execution_checks",
"agents_skills_speckit_converge_skill_user_input"
],
"2": [
"specify_scripts_powershell_common",
"specify_scripts_powershell_common_find_specifyroot",
"specify_scripts_powershell_common_format_speckitcommand",
"specify_scripts_powershell_common_get_currentbranch",
"specify_scripts_powershell_common_get_featurepathsenv",
"specify_scripts_powershell_common_get_invokeseparator",
"specify_scripts_powershell_common_get_normalizedpriority",
"specify_scripts_powershell_common_get_python3command",
"specify_scripts_powershell_common_get_reporoot",
"specify_scripts_powershell_common_get_sortedextensionids",
"specify_scripts_powershell_common_resolve_specifyinitdir",
"specify_scripts_powershell_common_resolve_template",
"specify_scripts_powershell_common_resolve_templatecontent",
"specify_scripts_powershell_common_save_featurejson",
"specify_scripts_powershell_common_test_dirhasfiles",
"specify_scripts_powershell_common_test_fileexists"
],
"3": [
"agents_skills_graphify_skill",
"agents_skills_graphify_skill_for_graphify_add_and_watch",
"agents_skills_graphify_skill_for_graphify_query",
"agents_skills_graphify_skill_for_the_commit_hook_and_native_claude_md_integration",
"agents_skills_graphify_skill_for_update_and_cluster_only",
"agents_skills_graphify_skill_graphify",
"agents_skills_graphify_skill_honesty_rules",
"agents_skills_graphify_skill_interpreter_guard_for_subcommands",
"agents_skills_graphify_skill_part_a_structural_extraction_for_code_files",
"agents_skills_graphify_skill_part_b_semantic_extraction_parallel_subagents",
"agents_skills_graphify_skill_part_c_merge_ast_semantic_into_final_extraction",
"agents_skills_graphify_skill_step_0_github_repos_and_multi_path_merge_only_if_a_url_or_several_paths",
"agents_skills_graphify_skill_step_1_ensure_graphify_is_installed",
"agents_skills_graphify_skill_step_2_5_video_and_audio_only_if_video_files_detected",
"agents_skills_graphify_skill_step_2_detect_files",
"agents_skills_graphify_skill_step_3_extract_entities_and_relationships",
"agents_skills_graphify_skill_step_4_5_graph_health_check_read_only_integrity_gate",
"agents_skills_graphify_skill_step_4_build_graph_cluster_analyze_generate_outputs",
"agents_skills_graphify_skill_step_5_label_communities",
"agents_skills_graphify_skill_step_6_generate_obsidian_vault_opt_in_html",
"agents_skills_graphify_skill_step_9_save_manifest_update_cost_tracker_clean_up_and_report",
"agents_skills_graphify_skill_steps_6b_8_wiki_neo4j_falkordb_svg_graphml_mcp_benchmark_only_on_their_flags",
"agents_skills_graphify_skill_usage",
"agents_skills_graphify_skill_what_graphify_is_for",
"agents_skills_graphify_skill_what_you_must_do_when_invoked"
],
"4": [
"agents_skills_speckit_analyze_skill",
"agents_skills_speckit_analyze_skill_7_provide_next_actions",
"agents_skills_speckit_analyze_skill_8_offer_remediation",
"agents_skills_speckit_analyze_skill_9_check_for_extension_hooks",
"agents_skills_speckit_analyze_skill_analysis_guidelines",
"agents_skills_speckit_analyze_skill_context",
"agents_skills_speckit_analyze_skill_context_efficiency",
"agents_skills_speckit_analyze_skill_goal",
"agents_skills_speckit_analyze_skill_operating_constraints",
"agents_skills_speckit_analyze_skill_operating_principles",
"agents_skills_speckit_analyze_skill_pre_execution_checks",
"agents_skills_speckit_analyze_skill_specification_analysis_report",
"agents_skills_speckit_analyze_skill_user_input"
],
"5": [
"agents_skills_speckit_analyze_skill_1_initialize_analysis_context",
"agents_skills_speckit_analyze_skill_2_load_artifacts_progressive_disclosure",
"agents_skills_speckit_analyze_skill_3_build_semantic_models",
"agents_skills_speckit_analyze_skill_4_detection_passes_token_efficient_analysis",
"agents_skills_speckit_analyze_skill_5_severity_assignment",
"agents_skills_speckit_analyze_skill_6_produce_compact_analysis_report",
"agents_skills_speckit_analyze_skill_a_duplication_detection",
"agents_skills_speckit_analyze_skill_b_ambiguity_detection",
"agents_skills_speckit_analyze_skill_c_underspecification",
"agents_skills_speckit_analyze_skill_d_constitution_alignment",
"agents_skills_speckit_analyze_skill_e_coverage_gaps",
"agents_skills_speckit_analyze_skill_execution_steps",
"agents_skills_speckit_analyze_skill_f_inconsistency"
],
"6": [
"specify_templates_spec_template",
"specify_templates_spec_template_assumptions",
"specify_templates_spec_template_edge_cases",
"specify_templates_spec_template_feature_specification_feature_name",
"specify_templates_spec_template_functional_requirements",
"specify_templates_spec_template_key_entities_include_if_feature_involves_data",
"specify_templates_spec_template_measurable_outcomes",
"specify_templates_spec_template_requirements_mandatory",
"specify_templates_spec_template_success_criteria_mandatory",
"specify_templates_spec_template_user_scenarios_testing_mandatory",
"specify_templates_spec_template_user_story_1_brief_title_priority_p1",
"specify_templates_spec_template_user_story_2_brief_title_priority_p2",
"specify_templates_spec_template_user_story_3_brief_title_priority_p3"
],
"7": [
"agents_rules_graphify",
"agents_rules_graphify_graphify"
],
"8": [
"agents_skills_speckit_plan_skill",
"agents_skills_speckit_plan_skill_completion_report",
"agents_skills_speckit_plan_skill_done_when",
"agents_skills_speckit_plan_skill_key_rules",
"agents_skills_speckit_plan_skill_mandatory_post_execution_hooks",
"agents_skills_speckit_plan_skill_outline",
"agents_skills_speckit_plan_skill_phase_0_outline_research",
"agents_skills_speckit_plan_skill_phase_1_design_contracts",
"agents_skills_speckit_plan_skill_phases",
"agents_skills_speckit_plan_skill_pre_execution_checks",
"agents_skills_speckit_plan_skill_user_input"
],
"9": [
"agents_skills_speckit_specify_skill",
"agents_skills_speckit_specify_skill_completion_report",
"agents_skills_speckit_specify_skill_done_when",
"agents_skills_speckit_specify_skill_for_ai_generation",
"agents_skills_speckit_specify_skill_mandatory_post_execution_hooks",
"agents_skills_speckit_specify_skill_outline",
"agents_skills_speckit_specify_skill_pre_execution_checks",
"agents_skills_speckit_specify_skill_quick_guidelines",
"agents_skills_speckit_specify_skill_section_requirements",
"agents_skills_speckit_specify_skill_success_criteria_guidelines",
"agents_skills_speckit_specify_skill_user_input"
],
"10": [
"agents_skills_speckit_tasks_skill",
"agents_skills_speckit_tasks_skill_checklist_format_required",
"agents_skills_speckit_tasks_skill_completion_report",
"agents_skills_speckit_tasks_skill_done_when",
"agents_skills_speckit_tasks_skill_mandatory_post_execution_hooks",
"agents_skills_speckit_tasks_skill_outline",
"agents_skills_speckit_tasks_skill_phase_structure",
"agents_skills_speckit_tasks_skill_pre_execution_checks",
"agents_skills_speckit_tasks_skill_task_generation_rules",
"agents_skills_speckit_tasks_skill_task_organization",
"agents_skills_speckit_tasks_skill_user_input"
],
"11": [
"specify_memory_constitution",
"specify_memory_constitution_core_principles",
"specify_memory_constitution_governance",
"specify_memory_constitution_principle_1_name",
"specify_memory_constitution_principle_2_name",
"specify_memory_constitution_principle_3_name",
"specify_memory_constitution_principle_4_name",
"specify_memory_constitution_principle_5_name",
"specify_memory_constitution_project_name_constitution",
"specify_memory_constitution_section_2_name",
"specify_memory_constitution_section_3_name"
],
"12": [
"specify_templates_constitution_template",
"specify_templates_constitution_template_core_principles",
"specify_templates_constitution_template_governance",
"specify_templates_constitution_template_principle_1_name",
"specify_templates_constitution_template_principle_2_name",
"specify_templates_constitution_template_principle_3_name",
"specify_templates_constitution_template_principle_4_name",
"specify_templates_constitution_template_principle_5_name",
"specify_templates_constitution_template_project_name_constitution",
"specify_templates_constitution_template_section_2_name",
"specify_templates_constitution_template_section_3_name"
],
"13": [
"agents_skills_graphify_references_exports",
"agents_skills_graphify_references_exports_graphify_reference_extra_exports_and_benchmark",
"agents_skills_graphify_references_exports_step_6b_wiki_only_if_wiki_flag",
"agents_skills_graphify_references_exports_step_7_neo4j_export_only_if_neo4j_or_neo4j_push_flag",
"agents_skills_graphify_references_exports_step_7a_falkordb_export_only_if_falkordb_or_falkordb_push_flag",
"agents_skills_graphify_references_exports_step_7b_svg_export_only_if_svg_flag",
"agents_skills_graphify_references_exports_step_7c_graphml_export_only_if_graphml_flag",
"agents_skills_graphify_references_exports_step_7d_mcp_server_only_if_mcp_flag",
"agents_skills_graphify_references_exports_step_8_token_reduction_benchmark_only_if_total_words_5000"
],
"14": [
"agents_skills_ponytail_skill",
"agents_skills_ponytail_skill_boundaries",
"agents_skills_ponytail_skill_intensity",
"agents_skills_ponytail_skill_output",
"agents_skills_ponytail_skill_persistence",
"agents_skills_ponytail_skill_ponytail",
"agents_skills_ponytail_skill_rules",
"agents_skills_ponytail_skill_the_ladder",
"agents_skills_ponytail_skill_when_not_to_be_lazy"
],
"15": [
"specify_templates_plan_template",
"specify_templates_plan_template_complexity_tracking",
"specify_templates_plan_template_constitution_check",
"specify_templates_plan_template_documentation_this_feature",
"specify_templates_plan_template_implementation_plan_feature",
"specify_templates_plan_template_project_structure",
"specify_templates_plan_template_source_code_repository_root",
"specify_templates_plan_template_summary",
"specify_templates_plan_template_technical_context"
],
"16": [
"agents_skills_ponytail_help_skill",
"agents_skills_ponytail_help_skill_configure_default_mode",
"agents_skills_ponytail_help_skill_deactivate",
"agents_skills_ponytail_help_skill_levels",
"agents_skills_ponytail_help_skill_more",
"agents_skills_ponytail_help_skill_ponytail_help",
"agents_skills_ponytail_help_skill_skills",
"agents_skills_ponytail_help_skill_update"
],
"17": [
"agents_skills_speckit_checklist_skill",
"agents_skills_speckit_checklist_skill_anti_examples_what_not_to_do",
"agents_skills_speckit_checklist_skill_checklist_purpose_unit_tests_for_english",
"agents_skills_speckit_checklist_skill_example_checklist_types_sample_items",
"agents_skills_speckit_checklist_skill_execution_steps",
"agents_skills_speckit_checklist_skill_post_execution_checks",
"agents_skills_speckit_checklist_skill_pre_execution_checks",
"agents_skills_speckit_checklist_skill_user_input"
],
"18": [
"agents_skills_speckit_clarify_skill",
"agents_skills_speckit_clarify_skill_completion_report",
"agents_skills_speckit_clarify_skill_done_when",
"agents_skills_speckit_clarify_skill_mandatory_post_execution_hooks",
"agents_skills_speckit_clarify_skill_outline",
"agents_skills_speckit_clarify_skill_pre_execution_checks",
"agents_skills_speckit_clarify_skill_user_input"
],
"19": [
"agents_skills_speckit_implement_skill",
"agents_skills_speckit_implement_skill_completion_report",
"agents_skills_speckit_implement_skill_done_when",
"agents_skills_speckit_implement_skill_mandatory_post_execution_hooks",
"agents_skills_speckit_implement_skill_outline",
"agents_skills_speckit_implement_skill_pre_execution_checks",
"agents_skills_speckit_implement_skill_user_input"
],
"20": [
"agents_skills_graphify_references_query",
"agents_skills_graphify_references_query_for_graphify_explain",
"agents_skills_graphify_references_query_for_graphify_path",
"agents_skills_graphify_references_query_graphify_reference_query_path_explain",
"agents_skills_graphify_references_query_step_0_constrained_query_expansion_required_before_traversal",
"agents_skills_graphify_references_query_step_1_traversal"
],
"21": [
"agents_skills_speckit_constitution_skill",
"agents_skills_speckit_constitution_skill_outline",
"agents_skills_speckit_constitution_skill_post_execution_checks",
"agents_skills_speckit_constitution_skill_pre_execution_checks",
"agents_skills_speckit_constitution_skill_scope_guard",
"agents_skills_speckit_constitution_skill_user_input"
],
"22": [
"specify_scripts_powershell_create_new_feature",
"specify_scripts_powershell_create_new_feature_convertto_cleanbranchname",
"specify_scripts_powershell_create_new_feature_get_branchname",
"specify_scripts_powershell_create_new_feature_get_fittedbranchname",
"specify_scripts_powershell_create_new_feature_get_highestnumberfromspecs",
"specify_scripts_powershell_create_new_feature_test_specprefixinuse"
],
"23": [
"agents_skills_ponytail_audit_skill",
"agents_skills_ponytail_audit_skill_boundaries",
"agents_skills_ponytail_audit_skill_hunt",
"agents_skills_ponytail_audit_skill_output",
"agents_skills_ponytail_audit_skill_tags"
],
"24": [
"agents_skills_ponytail_gain_skill",
"agents_skills_ponytail_gain_skill_boundaries",
"agents_skills_ponytail_gain_skill_honesty_boundary",
"agents_skills_ponytail_gain_skill_ponytail_gain",
"agents_skills_ponytail_gain_skill_scoreboard"
],
"25": [
"agents_skills_ponytail_review_skill",
"agents_skills_ponytail_review_skill_boundaries",
"agents_skills_ponytail_review_skill_examples",
"agents_skills_ponytail_review_skill_format",
"agents_skills_ponytail_review_skill_scoring"
],
"26": [
"agents_skills_speckit_taskstoissues_skill",
"agents_skills_speckit_taskstoissues_skill_outline",
"agents_skills_speckit_taskstoissues_skill_post_execution_checks",
"agents_skills_speckit_taskstoissues_skill_pre_execution_checks",
"agents_skills_speckit_taskstoissues_skill_user_input"
],
"27": [
"specify_templates_checklist_template",
"specify_templates_checklist_template_category_1",
"specify_templates_checklist_template_category_2",
"specify_templates_checklist_template_checklist_type_checklist_feature_name",
"specify_templates_checklist_template_notes"
],
"28": [
"agents_skills_graphify_references_add_watch",
"agents_skills_graphify_references_add_watch_for_graphify_add",
"agents_skills_graphify_references_add_watch_for_watch",
"agents_skills_graphify_references_add_watch_graphify_reference_add_a_url_and_watch_a_folder"
],
"29": [
"agents_skills_graphify_references_hooks",
"agents_skills_graphify_references_hooks_for_git_commit_hook",
"agents_skills_graphify_references_hooks_for_native_claude_md_integration",
"agents_skills_graphify_references_hooks_graphify_reference_commit_hook_and_native_claude_md_integration"
],
"30": [
"agents_skills_graphify_references_update",
"agents_skills_graphify_references_update_for_cluster_only",
"agents_skills_graphify_references_update_for_update_incremental_re_extraction",
"agents_skills_graphify_references_update_graphify_reference_incremental_update_and_cluster_only"
],
"31": [
"agents_skills_ponytail_debt_skill",
"agents_skills_ponytail_debt_skill_boundaries",
"agents_skills_ponytail_debt_skill_output",
"agents_skills_ponytail_debt_skill_scan"
],
"32": [
"agents_skills_graphify_references_github_and_merge",
"agents_skills_graphify_references_github_and_merge_graphify_reference_github_clone_and_cross_repo_merge",
"agents_skills_graphify_references_github_and_merge_step_0_clone_github_repo_s_only_if_a_github_url_was_given"
],
"33": [
"agents_skills_graphify_references_transcribe",
"agents_skills_graphify_references_transcribe_graphify_reference_transcribe_video_and_audio",
"agents_skills_graphify_references_transcribe_step_2_5_transcribe_video_audio_files_only_if_video_files_detected"
],
"34": [
"agents_skills_graphify_references_extraction_spec",
"agents_skills_graphify_references_extraction_spec_graphify_reference_extraction_subagent_prompt"
],
"35": [
"specify_scripts_powershell_check_prerequisites"
],
"36": [
"specify_scripts_powershell_resolve_template"
],
"37": [
"specify_scripts_powershell_setup_plan"
],
"38": [
"specify_scripts_powershell_setup_tasks"
],
"39": [
"agents_workflows_graphify",
"agents_workflows_graphify_workflow_graphify"
]
},
"cohesion": {
"0": 0.07407407407407407,
"1": 0.125,
"2": 0.225,
"3": 0.08,
"4": 0.15384615384615385,
"5": 0.15384615384615385,
"6": 0.15384615384615385,
"7": 1.0,
"8": 0.18181818181818182,
"9": 0.18181818181818182,
"10": 0.18181818181818182,
"11": 0.18181818181818182,
"12": 0.18181818181818182,
"13": 0.2222222222222222,
"14": 0.2222222222222222,
"15": 0.2222222222222222,
"16": 0.25,
"17": 0.25,
"18": 0.2857142857142857,
"19": 0.2857142857142857,
"20": 0.3333333333333333,
"21": 0.3333333333333333,
"22": 0.4,
"23": 0.4,
"24": 0.4,
"25": 0.4,
"26": 0.4,
"27": 0.4,
"28": 0.5,
"29": 0.5,
"30": 0.5,
"31": 0.5,
"32": 0.6666666666666666,
"33": 0.6666666666666666,
"34": 1.0,
"35": 1.0,
"36": 1.0,
"37": 1.0,
"38": 1.0,
"39": 1.0
},
"gods": [
{
"id": "specify_templates_tasks_template_tasks_feature_name",
"label": "Tasks: [FEATURE NAME]",
"degree": 13
},
{
"id": "agents_skills_graphify_skill_what_you_must_do_when_invoked",
"label": "What You Must Do When Invoked",
"degree": 12
},
{
"id": "agents_skills_graphify_skill_graphify",
"label": "/graphify",
"degree": 10
},
{
"id": "agents_skills_graphify_references_exports_graphify_reference_extra_exports_and_benchmark",
"label": "graphify reference: extra exports and benchmark",
"degree": 8
},
{
"id": "agents_skills_ponytail_skill_ponytail",
"label": "Ponytail",
"degree": 8
},
{
"id": "agents_skills_speckit_converge_skill_execution_steps",
"label": "Execution Steps",
"degree": 7
},
{
"id": "agents_skills_ponytail_help_skill_ponytail_help",
"label": "Ponytail Help",
"degree": 7
},
{
"id": "agents_skills_speckit_analyze_skill_4_detection_passes_token_efficient_analysis",
"label": "4. Detection Passes (Token-Efficient Analysis)",
"degree": 7
},
{
"id": "agents_skills_speckit_analyze_skill_execution_steps",
"label": "Execution Steps",
"degree": 7
},
{
"id": "specify_memory_constitution_core_principles",
"label": "Core Principles",
"degree": 6
}
],
"surprises": [],
"questions": [
{
"type": "bridge_node",
"question": "Why does `Execution Steps` connect `Analysis Detection` to `Specification Analysis`?",
"why": "High betweenness centrality (0.004) - this node is a cross-community bridge."
},
{
"type": "isolated_nodes",
"question": "What connects `Format: `[ID] [P?] [Story] Description``, `Implementation for User Story 1`, `Implementation for User Story 2` to the rest of the system?",
"why": "212 weakly-connected nodes found - possible documentation gaps or missing edges."
},
{
"type": "low_cohesion",
"question": "Should `Task Planning` be split into smaller, more focused modules?",
"why": "Cohesion score 0.07407407407407407 - nodes in this community are weakly interconnected."
},
{
"type": "low_cohesion",
"question": "Should `Convergence Workflow` be split into smaller, more focused modules?",
"why": "Cohesion score 0.125 - nodes in this community are weakly interconnected."
},
{
"type": "low_cohesion",
"question": "Should `Graphify Commands` be split into smaller, more focused modules?",
"why": "Cohesion score 0.08 - nodes in this community are weakly interconnected."
}
]
}
@@ -0,0 +1,172 @@
{
"0": "Task Planning",
"1": "Convergence Workflow",
"2": "SpecKit Utilities",
"3": "Graphify Commands",
"4": "speckit-analyze/SKILL.md",
"5": "Tasks: Multilingual NLP Entity Inherence Classifier (POC)",
"6": "Feature Specification Template",
"7": "Graphify Rules",
"8": "Implementation Planning",
"9": "Feature Specification",
"10": "Task Generation",
"11": "Project Constitution",
"12": "Constitution Template",
"13": "Graphify Exports",
"14": "Ponytail Configuration",
"15": "Implementation Planning Template",
"16": "Ponytail Help",
"17": "Checklist Generation",
"18": "Clarification Workflow",
"19": "Implementation Workflow",
"20": "Graph Query",
"21": "Constitution Workflow",
"22": "Feature Branch Creation",
"23": "Ponytail Audit",
"24": "Ponytail Metrics",
"25": "Ponytail Review",
"26": "Task Issue Conversion",
"27": "Checklist Template",
"28": "Graphify Watch Mode",
"29": "Graphify Hooks",
"30": "Graphify Updates",
"31": "Ponytail Debt",
"32": "Repository Merge",
"33": "Media Transcription",
"34": "Extraction Specification",
"35": "Prerequisite Checks",
"36": "Template Resolution",
"37": "Plan Setup",
"38": "Task Setup",
"39": "Graphify Workflows",
"40": "main",
"41": "1. Technical Decisions & Tradeoffs",
"42": "1. Input Schemas",
"43": "2. Basic CLI Usage Examples",
"44": "2. Standard Streams & Exit Codes",
"45": "ClassificationResult",
"46": "test_adversarial.py",
"47": "LLMFallbackAdapter",
"48": "test_convert_article_to_markdown.py",
"49": "content_northvolt_de.md",
"50": "content_presal_pt.md",
"51": "content_tangential_es.md",
"52": "adapters/__init__.py",
"53": "src/__init__.py",
"54": "de/contextual.md",
"55": "de/direct.md",
"56": "de/not_related.md",
"57": "de/tangential.md",
"58": "en/contextual.md",
"59": "en/direct.md",
"60": "en/not_related.md",
"61": "en/tangential.md",
"62": "es/contextual.md",
"63": "es/direct.md",
"64": "es/not_related.md",
"65": "es/tangential.md",
"66": "fr/contextual.md",
"67": "fr/direct.md",
"68": "fr/not_related.md",
"69": "fr/tangential.md",
"70": "it/contextual.md",
"71": "it/direct.md",
"72": "it/not_related.md",
"73": "it/tangential.md",
"74": "pt/contextual.md",
"75": "pt/direct.md",
"76": "pt/not_related.md",
"77": "pt/tangential.md",
"78": "tests/__init__.py",
"79": "text-nlp-classifier",
"80": "test_extract_article_contents.py",
"81": "Extrator de Notícias do Google News — Guia Completo de Funcionamento",
"82": "extract_google_news.py",
"83": "ExtractionResult",
"84": "test_extract_google_news.py",
"85": "Implementation Tasks: Google News Headlines Extractor",
"86": "Feature Specification: Google News Headlines Extractor",
"87": "2. Cenários Práticos de Uso",
"88": "Implementation Plan: Google News Headlines Extractor",
"89": "scripts/__init__.py",
"90": "SearchQuery",
"91": "1. Technical Decisions & Tradeoffs",
"92": "General Readiness Checklist: Google News Headlines Extractor",
"93": "1. Entidades de Domínio & DTOs",
"94": "Specification Quality Checklist: Google News Headlines Extractor",
"95": "CLI Contract: Google News Headlines Extractor",
"96": "readiness.md",
"97": "🧠 TextNLPClassifierApp",
"98": "Extraction Pipeline Checklist: Article Content Multi-Engine Extractor",
"99": "parametrize",
"100": "main",
"101": "classifier.py",
"102": "Feature Specification: Multilingual NLP Entity Inherence Classifier (POC)",
"103": "4. Requisitos Funcionais (FR)",
"104": "Tasks: Article Content Multi-Engine Extractor",
"105": "Tasks: Convert Article JSON to Markdown",
"106": "Implementation Plan: Article Content Multi-Engine Extractor",
"107": "2. Cenários de Validação",
"108": "1. Technical Decisions & Tradeoffs",
"109": "Implementation Plan: Multilingual NLP Entity Inherence Classifier (POC)",
"110": "POC Readiness & Requirements Quality Checklist: Multilingual NLP Entity Inherence Classifier",
"111": "Specification Quality Checklist: Multilingual NLP Entity Inherence Classifier",
"112": "CLI Contract: Article Content Multi-Engine Extractor",
"113": "001-multilingual-entity-classifier/spec.md",
"114": "JSON Schema Contract: Article Content Multi-Engine Extractor",
"115": "PRD — Seleção determinística da biblioteca de extração de conteúdo",
"116": "select_article_extractor.py",
"117": "1. Text Normalization Pipeline",
"118": "Tasks: Deterministic Article Content Selection",
"119": "select_article_extractor",
"120": "process_batch",
"121": "test_models.py",
"122": "test_select_article_extractor.py",
"123": "Feature Specification: Deterministic Content Selection",
"124": "2. Entity Descriptions & Fields",
"125": "Implementation Plan: Deterministic Article Content Selection",
"126": "Deterministic Content Selection Checklist: End-to-End Requirements Quality",
"127": "Quickstart: Deterministic Article Content Selection",
"128": "Specification Quality Checklist: Deterministic Content Selection",
"129": "CLI Interface Contract: Deterministic Article Content Selection",
"130": "004-deterministic-content-selection/spec.md",
"131": "convert_article_to_markdown.py",
"132": "8. Regras funcionais",
"133": "12. Critérios de aceite",
"134": "PRD — Conversão de artigo JSON para Markdown",
"135": "resolve_article_body",
"136": "Implementation Plan: Convert Article JSON to Markdown",
"137": "2. Technical Decisions & Research Findings",
"138": "Feature Specification: Convert Article JSON to Markdown",
"139": "Markdown Conversion Checklist: End-to-End Requirements Quality",
"140": "convert_article",
"141": "005-convert-json-markdown/plan.md",
"142": "Quickstart: Convert Article JSON to Markdown",
"143": "1. Domain Entities & Schemas",
"144": "11. Requisitos não funcionais",
"145": "parse_arguments",
"146": "Specification Quality Checklist: Convert Article JSON to Markdown",
"147": "CLI Contract: `convert_article_to_markdown.py`",
"148": "9. Interface CLI",
"149": "sample_rss_xml",
"150": "13. Estratégia de testes",
"151": "6. Contrato de entrada",
"152": "ECPSnapshot",
"153": "convert_html_to_markdown",
"154": "JSON Schema Contract: Deterministic Article Content Selection",
"155": "5. Escopo",
"156": "Los puntajes de River vs. Independiente Santa Fe, por la Copa Sudamericana - TyC Sports",
"157": "valid_newspaper4k.md",
"158": "valid_readability.md",
"159": "test_normalize_list_author_url_filtering",
"160": "test_normalize_date_invalid_and_placeholders",
"161": "test_normalize_list_deduplication_preserves_case_and_order",
"162": "test_normalize_date_iso_8601_variants",
"163": "test_metadata_priority_original_url_all_fallbacks",
"164": "test_normalize_scalar_non_string_types",
"165": "InherenceClassifier",
"166": "remove_duplicate_initial_h1",
"167": "test_normalize_scalar_whitespace_collapsing",
"168": ".disambiguate",
"169": "test_funnel_cli_subprocess_end_to_end"
}
+692
View File
@@ -0,0 +1,692 @@
# Graph Report - TextNLPClassifierApp (2026-08-21)
## Corpus Check
- 203 files · ~114,931 words
- Verdict: corpus is large enough that graph structure adds value.
## Summary
- 1651 nodes · 2220 edges · 170 communities (122 shown, 48 thin omitted)
- Extraction: 94% EXTRACTED · 6% INFERRED · 0% AMBIGUOUS · INFERRED: 127 edges (avg confidence: 0.95)
- Token cost: 0 input · 0 output
## Graph Freshness
- Built from commit: `2cdd3547`
- Run `git rev-parse HEAD` and compare to check if the graph is stale.
- Run `graphify update .` after code changes (no API cost).
## Community Hubs (Navigation)
- Task Planning
- Convergence Workflow
- SpecKit Utilities
- Graphify Commands
- speckit-analyze/SKILL.md
- Tasks: Multilingual NLP Entity Inherence Classifier (POC)
- Feature Specification Template
- Graphify Rules
- Implementation Planning
- Feature Specification
- Task Generation
- Project Constitution
- Constitution Template
- Graphify Exports
- Ponytail Configuration
- Implementation Planning Template
- Ponytail Help
- Checklist Generation
- Clarification Workflow
- Implementation Workflow
- Graph Query
- Constitution Workflow
- Feature Branch Creation
- Ponytail Audit
- Ponytail Metrics
- Ponytail Review
- Task Issue Conversion
- Checklist Template
- Graphify Watch Mode
- Graphify Hooks
- Graphify Updates
- Ponytail Debt
- Repository Merge
- Media Transcription
- Extraction Specification
- Graphify Workflows
- main
- 1. Technical Decisions & Tradeoffs
- 1. Input Schemas
- 2. Basic CLI Usage Examples
- 2. Standard Streams & Exit Codes
- ClassificationResult
- test_adversarial.py
- LLMFallbackAdapter
- test_convert_article_to_markdown.py
- content_northvolt_de.md
- content_presal_pt.md
- content_tangential_es.md
- adapters/__init__.py
- src/__init__.py
- de/contextual.md
- de/direct.md
- de/not_related.md
- de/tangential.md
- en/contextual.md
- en/direct.md
- en/not_related.md
- en/tangential.md
- es/contextual.md
- es/direct.md
- es/not_related.md
- es/tangential.md
- fr/contextual.md
- fr/direct.md
- fr/not_related.md
- fr/tangential.md
- it/contextual.md
- it/direct.md
- it/not_related.md
- it/tangential.md
- pt/contextual.md
- pt/direct.md
- pt/not_related.md
- pt/tangential.md
- tests/__init__.py
- text-nlp-classifier
- test_extract_article_contents.py
- Extrator de Notícias do Google News — Guia Completo de Funcionamento
- extract_google_news.py
- ExtractionResult
- test_extract_google_news.py
- Implementation Tasks: Google News Headlines Extractor
- Feature Specification: Google News Headlines Extractor
- 2. Cenários Práticos de Uso
- Implementation Plan: Google News Headlines Extractor
- scripts/__init__.py
- SearchQuery
- 1. Technical Decisions & Tradeoffs
- General Readiness Checklist: Google News Headlines Extractor
- 1. Entidades de Domínio & DTOs
- Specification Quality Checklist: Google News Headlines Extractor
- CLI Contract: Google News Headlines Extractor
- 🧠 TextNLPClassifierApp
- Extraction Pipeline Checklist: Article Content Multi-Engine Extractor
- parametrize
- main
- classifier.py
- Feature Specification: Multilingual NLP Entity Inherence Classifier (POC)
- 4. Requisitos Funcionais (FR)
- Tasks: Article Content Multi-Engine Extractor
- Tasks: Convert Article JSON to Markdown
- Implementation Plan: Article Content Multi-Engine Extractor
- 2. Cenários de Validação
- 1. Technical Decisions & Tradeoffs
- Implementation Plan: Multilingual NLP Entity Inherence Classifier (POC)
- POC Readiness & Requirements Quality Checklist: Multilingual NLP Entity Inherence Classifier
- Specification Quality Checklist: Multilingual NLP Entity Inherence Classifier
- CLI Contract: Article Content Multi-Engine Extractor
- JSON Schema Contract: Article Content Multi-Engine Extractor
- PRD — Seleção determinística da biblioteca de extração de conteúdo
- select_article_extractor.py
- 1. Text Normalization Pipeline
- Tasks: Deterministic Article Content Selection
- select_article_extractor
- process_batch
- test_models.py
- test_select_article_extractor.py
- Feature Specification: Deterministic Content Selection
- 2. Entity Descriptions & Fields
- Implementation Plan: Deterministic Article Content Selection
- Deterministic Content Selection Checklist: End-to-End Requirements Quality
- Quickstart: Deterministic Article Content Selection
- Specification Quality Checklist: Deterministic Content Selection
- CLI Interface Contract: Deterministic Article Content Selection
- convert_article_to_markdown.py
- 8. Regras funcionais
- 12. Critérios de aceite
- PRD — Conversão de artigo JSON para Markdown
- resolve_article_body
- Implementation Plan: Convert Article JSON to Markdown
- 2. Technical Decisions & Research Findings
- Feature Specification: Convert Article JSON to Markdown
- Markdown Conversion Checklist: End-to-End Requirements Quality
- convert_article
- 005-convert-json-markdown/plan.md
- Quickstart: Convert Article JSON to Markdown
- 1. Domain Entities & Schemas
- 11. Requisitos não funcionais
- parse_arguments
- Specification Quality Checklist: Convert Article JSON to Markdown
- CLI Contract: `convert_article_to_markdown.py`
- 9. Interface CLI
- sample_rss_xml
- 13. Estratégia de testes
- 6. Contrato de entrada
- ECPSnapshot
- convert_html_to_markdown
- JSON Schema Contract: Deterministic Article Content Selection
- 5. Escopo
- Los puntajes de River vs. Independiente Santa Fe, por la Copa Sudamericana - TyC Sports
- valid_newspaper4k.md
- valid_readability.md
- test_normalize_list_author_url_filtering
- test_normalize_date_invalid_and_placeholders
- test_normalize_list_deduplication_preserves_case_and_order
- test_normalize_date_iso_8601_variants
- test_metadata_priority_original_url_all_fallbacks
- test_normalize_scalar_non_string_types
- InherenceClassifier
- remove_duplicate_initial_h1
- test_normalize_scalar_whitespace_collapsing
- .disambiguate
- test_funnel_cli_subprocess_end_to_end
## God Nodes (most connected - your core abstractions)
1. `ECPSnapshot` - 78 edges
2. `InherenceClassifier` - 62 edges
3. `DecisionCategory` - 62 edges
4. `LLMFallbackAdapter` - 48 edges
5. `ClassificationResult` - 29 edges
6. `select_article_extractor()` - 23 edges
7. `ExtractorName` - 21 edges
8. `main()` - 20 edges
9. `PRD — Conversão de artigo JSON para Markdown` - 16 edges
10. `process_batch()` - 15 edges
## Surprising Connections (you probably didn't know these)
- `main()` --uses--> `ECPSnapshot` [INFERRED]
classify.py → src/models.py
- `main()` --uses--> `ErrorCode` [INFERRED]
classify.py → src/models.py
- `test_extract_google_news_orchestration_mocked()` --uses--> `ExtractionResult` [INFERRED]
tests/test_extract_google_news.py → scripts/extract_google_news.py
- `test_llm_adapter_interface()` --calls--> `LLMFallbackAdapter` [EXTRACTED]
tests/test_adapters.py → src/adapters/llm.py
- `classifier()` --uses--> `InherenceClassifier` [INFERRED]
tests/test_benchmark_24.py → src/classifier.py
## Import Cycles
- None detected.
## Communities (170 total, 48 thin omitted)
### Community 0 - "Task Planning"
Cohesion: 0.07
Nodes (26): Dependencies & Execution Order, Format: `[ID] [P?] [Story] Description`, Implementation for User Story 1, Implementation for User Story 2, Implementation for User Story 3, Implementation Strategy, Incremental Delivery, MVP First (User Story 1 Only) (+18 more)
### Community 1 - "Convergence Workflow"
Cohesion: 0.12
Nodes (15): 1. Initialize Convergence Context, 2. Load Artifacts (Progressive Disclosure), 3. Build the Intent Inventory, 4. Assess the Codebase and Classify Findings, 5. Assign Severity, 6. Present the In-Session Findings Summary, 7. Append Convergence Tasks (or report converged), 8. Provide Next Actions (Handoff) (+7 more)
### Community 2 - "SpecKit Utilities"
Cohesion: 0.23
Nodes (13): Find-SpecifyRoot(), Format-SpecKitCommand(), Get-CurrentBranch(), Get-FeaturePathsEnv(), Get-InvokeSeparator(), Get-NormalizedPriority(), Get-Python3Command(), Get-RepoRoot() (+5 more)
### Community 3 - "Graphify Commands"
Cohesion: 0.08
Nodes (24): For /graphify add and --watch, For /graphify query, For the commit hook and native CLAUDE.md integration, For --update and --cluster-only, /graphify, Honesty Rules, Interpreter guard for subcommands, Part A - Structural extraction for code files (+16 more)
### Community 4 - "speckit-analyze/SKILL.md"
Cohesion: 0.08
Nodes (25): 1. Initialize Analysis Context, 2. Load Artifacts (Progressive Disclosure), 3. Build Semantic Models, 4. Detection Passes (Token-Efficient Analysis), 5. Severity Assignment, 6. Produce Compact Analysis Report, 7. Provide Next Actions, 8. Offer Remediation (+17 more)
### Community 5 - "Tasks: Multilingual NLP Entity Inherence Classifier (POC)"
Cohesion: 0.15
Nodes (13): Dependencies & Execution Order, Implementation for User Story 1, Implementation for User Story 2, Implementation for User Story 3, Implementation Strategy, Phase 1: Setup (Shared Infrastructure), Phase 2: Foundational (Blocking Prerequisites), Phase 3: User Story 1 - Core Tier 1 Deterministic Classification & CLI (Priority: P1) [MVP] (+5 more)
### Community 6 - "Feature Specification Template"
Cohesion: 0.15
Nodes (12): Assumptions, Edge Cases, Feature Specification: [FEATURE NAME], Functional Requirements, Key Entities *(include if feature involves data)*, Measurable Outcomes, Requirements *(mandatory)*, Success Criteria *(mandatory)* (+4 more)
### Community 8 - "Implementation Planning"
Cohesion: 0.18
Nodes (10): Completion Report, Done When, Key rules, Mandatory Post-Execution Hooks, Outline, Phase 0: Outline & Research, Phase 1: Design & Contracts, Phases (+2 more)
### Community 9 - "Feature Specification"
Cohesion: 0.18
Nodes (10): Completion Report, Done When, For AI Generation, Mandatory Post-Execution Hooks, Outline, Pre-Execution Checks, Quick Guidelines, Section Requirements (+2 more)
### Community 10 - "Task Generation"
Cohesion: 0.18
Nodes (10): Checklist Format (REQUIRED), Completion Report, Done When, Mandatory Post-Execution Hooks, Outline, Phase Structure, Pre-Execution Checks, Task Generation Rules (+2 more)
### Community 11 - "Project Constitution"
Cohesion: 0.18
Nodes (10): Core Principles, Governance, [PRINCIPLE_1_NAME], [PRINCIPLE_2_NAME], [PRINCIPLE_3_NAME], [PRINCIPLE_4_NAME], [PRINCIPLE_5_NAME], [PROJECT_NAME] Constitution (+2 more)
### Community 12 - "Constitution Template"
Cohesion: 0.18
Nodes (10): Core Principles, Governance, [PRINCIPLE_1_NAME], [PRINCIPLE_2_NAME], [PRINCIPLE_3_NAME], [PRINCIPLE_4_NAME], [PRINCIPLE_5_NAME], [PROJECT_NAME] Constitution (+2 more)
### Community 13 - "Graphify Exports"
Cohesion: 0.22
Nodes (8): graphify reference: extra exports and benchmark, Step 6b - Wiki (only if --wiki flag), Step 7 - Neo4j export (only if --neo4j or --neo4j-push flag), Step 7a - FalkorDB export (only if --falkordb or --falkordb-push flag), Step 7b - SVG export (only if --svg flag), Step 7c - GraphML export (only if --graphml flag), Step 7d - MCP server (only if --mcp flag), Step 8 - Token reduction benchmark (only if total_words > 5000)
### Community 14 - "Ponytail Configuration"
Cohesion: 0.22
Nodes (8): Boundaries, Intensity, Output, Persistence, Ponytail, Rules, The ladder, When NOT to be lazy
### Community 15 - "Implementation Planning Template"
Cohesion: 0.22
Nodes (8): Complexity Tracking, Constitution Check, Documentation (this feature), Implementation Plan: [FEATURE], Project Structure, Source Code (repository root), Summary, Technical Context
### Community 16 - "Ponytail Help"
Cohesion: 0.25
Nodes (7): Configure Default Mode, Deactivate, Levels, More, Ponytail Help, Skills, Update
### Community 17 - "Checklist Generation"
Cohesion: 0.25
Nodes (7): Anti-Examples: What NOT To Do, Checklist Purpose: "Unit Tests for English", Example Checklist Types & Sample Items, Execution Steps, Post-Execution Checks, Pre-Execution Checks, User Input
### Community 18 - "Clarification Workflow"
Cohesion: 0.29
Nodes (6): Completion Report, Done When, Mandatory Post-Execution Hooks, Outline, Pre-Execution Checks, User Input
### Community 19 - "Implementation Workflow"
Cohesion: 0.29
Nodes (6): Completion Report, Done When, Mandatory Post-Execution Hooks, Outline, Pre-Execution Checks, User Input
### Community 20 - "Graph Query"
Cohesion: 0.33
Nodes (5): For /graphify explain, For /graphify path, graphify reference: query, path, explain, Step 0 — Constrained query expansion (REQUIRED before traversal), Step 1 — Traversal
### Community 21 - "Constitution Workflow"
Cohesion: 0.33
Nodes (5): Outline, Post-Execution Checks, Pre-Execution Checks, Scope Guard, User Input
### Community 23 - "Ponytail Audit"
Cohesion: 0.40
Nodes (4): Boundaries, Hunt, Output, Tags
### Community 24 - "Ponytail Metrics"
Cohesion: 0.40
Nodes (4): Boundaries, Honesty boundary, Ponytail Gain, Scoreboard
### Community 25 - "Ponytail Review"
Cohesion: 0.40
Nodes (4): Boundaries, Examples, Format, Scoring
### Community 26 - "Task Issue Conversion"
Cohesion: 0.40
Nodes (4): Outline, Post-Execution Checks, Pre-Execution Checks, User Input
### Community 27 - "Checklist Template"
Cohesion: 0.40
Nodes (4): [Category 1], [Category 2], [CHECKLIST TYPE] Checklist: [FEATURE NAME], Notes
### Community 28 - "Graphify Watch Mode"
Cohesion: 0.50
Nodes (3): For /graphify add, For --watch, graphify reference: add a URL and watch a folder
### Community 29 - "Graphify Hooks"
Cohesion: 0.50
Nodes (3): For git commit hook, For native CLAUDE.md integration, graphify reference: commit hook and native CLAUDE.md integration
### Community 30 - "Graphify Updates"
Cohesion: 0.50
Nodes (3): For --cluster-only, For --update (incremental re-extraction), graphify reference: incremental update and cluster-only
### Community 31 - "Ponytail Debt"
Cohesion: 0.50
Nodes (3): Boundaries, Output, Scan
### Community 40 - "main"
Cohesion: 0.14
Nodes (17): ArgumentParser, CaptureFixture, build_parser(), main(), Cria e configura o parser de argumentos CLI., Ponto de entrada do script CLI., Path, Valida execução padrão do CLI com saída JSON no stdout. (+9 more)
### Community 41 - "1. Technical Decisions & Tradeoffs"
Cohesion: 0.22
Nodes (8): 1. Technical Decisions & Tradeoffs, 2. Standardized Error Handling Strategy, Decision 1: Execution Engine & CLI Architecture, Decision 2: Multilingual Language Detection & Normalization (Tier 1 Core), Decision 3: Materialized ECP Snapshot Contract & Matching Logic, Decision 4: Tier 2 (Embeddings) & Tier 3 (LLM) Optional Adapters, Decision 5: Controlled 24-Case POC Benchmark Suite, Technical Research & Architecture Decisions (POC)
### Community 42 - "1. Input Schemas"
Cohesion: 0.25
Nodes (7): 1.1 ECP Snapshot Schema (`snapshot.json`), 1.2 Content Item Schema (`content.md`), 1. Input Schemas, 2.1 Classification Success Result Schema (`result.json`), 2.2 Error Result Schema, 2. Output Schemas, Data Models & Schemas (POC)
### Community 43 - "2. Basic CLI Usage Examples"
Cohesion: 0.25
Nodes (7): 1. Prerequisites & Installation, 2.1 Direct Inherence (Portuguese), 2.2 Contextual Inherence via Graph Snapshot (German), 2.3 Tangential Mention (Spanish), 2. Basic CLI Usage Examples, 3. Running the Controlled 24-Case Benchmark, Quickstart & Validation Guide (POC)
### Community 44 - "2. Standard Streams & Exit Codes"
Cohesion: 0.29
Nodes (6): 1.1 Arguments & Options, 1. Command Line Interface, 2.1 Exit Codes, 2.2 Standard Output (`stdout`) / Standard Error (`stderr`), 2. Standard Streams & Exit Codes, CLI Contract & Interface Specification (POC)
### Community 45 - "ClassificationResult"
Cohesion: 0.10
Nodes (19): ABC, BaseNLPAdapter, Base abstract adapter interface for optional Tier 2 / Tier 3 NLP enhancers., Abstract interface for pluggable NLP classification adapters., Return True if the underlying provider or model is installed and configured., Compute semantic similarity score between text and a set of candidate terms., Optionally refine an ambiguous classification result., LocalEmbeddingsAdapter (+11 more)
### Community 46 - "test_adversarial.py"
Cohesion: 0.10
Nodes (21): RelatedEntity, Adversarial and robustness test suite for Multilingual NLP Entity Inherence…, Run CLI via subprocess without --output and verify stdout is pure parseable…, Run CLI via subprocess with empty content and verify error code and exit code., Content about city/state governance of São Paulo against ECP for São Paulo FC., Run CLI via subprocess with missing target_name and verify error payload., Run CLI via subprocess with corrupted JSON and verify error payload., High-weight related entity mentioned in passing without required domain anchors. (+13 more)
### Community 47 - "LLMFallbackAdapter"
Cohesion: 0.06
Nodes (43): LLMFallbackAdapter, Optional adapter for LLM fallback boundary disambiguation., Any, parametrize, Suíte de Testes Exaustiva para o Classificador de Inerência (classify.py e…, Cenário 4.1: Caso ambíguo elevado para DIRECT_INHERENT pelo LLM., Cenário 4.2: Caso ambíguo elevado para CONTEXTUAL_INHERENT pelo LLM., Cenário 4.3: LLM confirma categoricamente que a menção é periférica /… (+35 more)
### Community 48 - "test_convert_article_to_markdown.py"
Cohesion: 0.08
Nodes (25): Suíte de Testes Automatizados para Conversão de Artigo JSON para Markdown.…, Testa divisão por ponto e vírgula na string e vírgulas em elementos de lista…, Valida parsing de datas no formato RFC 2822 (usado em feeds RSS e cabeçalhos…, Valida a cadeia de fallback completa para o campo TÍTULO (6 níveis)., Garante que subtítulo idêntico ao título seja automaticamente omitido (None)., Valida decodificação de entidades HTML nomeadas e numéricas., Garante correspondência exata byte a byte para Trafilatura, Newspaper4k e…, Garante que múltiplas execuções no mesmo arquivo produzam hashes SHA-256… (+17 more)
### Community 80 - "test_extract_article_contents.py"
Cohesion: 0.06
Nodes (56): ArticleCrawler, extract_all_engines(), ExtractedArticle, ExtractionBatchReport, InputArticle, load_search_json(), log_info(), main() (+48 more)
### Community 81 - "Extrator de Notícias do Google News — Guia Completo de Funcionamento"
Cohesion: 0.08
Nodes (24): 1. Visão geral (arquitetura), 2.1 DTO de entrada (`googlenews_etl/application/dtos/extract_news_dto.py`), 2.2 Value Object de validação (`googlenews_etl/domain/entities/search_query.py`), 2. Entrada, 3.1 O caso de uso (`googlenews_etl/application/use_cases/extract_news_use_case.py`), 3.2 A porta (`googlenews_etl/domain/ports/news_extractor_port.py`), 3.3.1 Inicialização: sessão HTTP com impersonação de browser, 3.3.2 Mapeamento idioma → parâmetros `hl`/`gl` (`_get_hl_gl`) (+16 more)
### Community 82 - "extract_google_news.py"
Cohesion: 0.15
Nodes (18): extract_google_news(), _fetch_rss_content(), get_hl_gl_ceid(), NewsArticle, _normalize_text_for_comparison(), parse_google_news_rss(), Mapeia idioma e locale para os parâmetros hl, gl e ceid do Google News., Remove pontuação e espaços extras para comparação de redundância. (+10 more)
### Community 83 - "ExtractionResult"
Cohesion: 0.29
Nodes (5): ExtractionResult, Any, Resultado consolidado da extração., Valida E2E o fluxo completo de busca, parsing e resolução de URLs reais ao vivo., test_e2e_extract_google_news_live_pipeline()
### Community 84 - "test_extract_google_news.py"
Cohesion: 0.15
Nodes (15): Resolve a URL intermediária do Google News para a URL real do veículo., resolve_article_url(), Testes unitários e de integração para o Extrator de Manchetes do Google News.…, Valida fallback gracioso de URL quando não é link do Google News ou em erro., Valida resolução bem-sucedida de URL do Google News para o portal destino., Valida E2E que o decodificador resolve uma URL real do Google News para o…, Valida o mapeamento padrão de idiomas para pares (hl, gl, ceid)., Valida a sobrescrita geográfica quando o argumento locale é especificado. (+7 more)
### Community 85 - "Implementation Tasks: Google News Headlines Extractor"
Cohesion: 0.14
Nodes (14): Implementation for User Story 1, Implementation for User Story 2, Implementation for User Story 3, Implementation Tasks: Google News Headlines Extractor, Phase 1: Setup (Shared Infrastructure), Phase 2: Foundational (Blocking Prerequisites), Phase 3: User Story 1 - Extração Básica de Notícias por Assunto e Idioma (Priority: P1) 🌟 MVP, Phase 4: User Story 2 - Filtragem Regional e Edição Geográfica (Priority: P2) (+6 more)
### Community 86 - "Feature Specification: Google News Headlines Extractor"
Cohesion: 0.18
Nodes (11): Clarifications, Edge Cases, Feature Specification: Google News Headlines Extractor, Functional Requirements, Requirements *(mandatory)*, Session 2026-08-20, Success Criteria *(mandatory)*, User Scenarios & Testing *(mandatory)* (+3 more)
### Community 87 - "2. Cenários Práticos de Uso"
Cohesion: 0.20
Nodes (9): 1. Pré-requisitos e Instalação, 2. Cenários Práticos de Uso, 3. Validação dos Testes Automatizados e Linter, Cenário 1: River Plate — Argentina (Espanhol / 2 Páginas / Salvar em Arquivo), Cenário 2: Cruzeiro — Brasil (Português / Formatado no Terminal), Cenário 3: Fórmula 1 — Reino Unido (Inglês), Cenário 4: Integração em Pipeline com `jq` (Modo Silencioso), Cenário 5: Extração Rápida com Links Brutos (Sem Resolução de URLs) (+1 more)
### Community 88 - "Implementation Plan: Google News Headlines Extractor"
Cohesion: 0.29
Nodes (7): Architecture & Pipeline, Documentation (this feature), Implementation Plan: Google News Headlines Extractor, Project Structure, Source Code, Summary, Technical Context
### Community 90 - "SearchQuery"
Cohesion: 0.20
Nodes (6): Value Object com parâmetros de busca validados., SearchQuery, Valida a consolidação do ExtractionResult a partir da busca mockada com URLs…, Valida as regras de negócio e limites de SearchQuery., test_extract_google_news_orchestration_mocked(), test_search_query_validation()
### Community 91 - "1. Technical Decisions & Tradeoffs"
Cohesion: 0.25
Nodes (7): 1. Technical Decisions & Tradeoffs, Decision 1: Motor de Requisição e Scraping com `foxcape` em Modo Headless, Decision 2: Endpoint RSS do Google News vs. Scraping de DOM, Decision 3: Mapeamento de Idioma e Locale (`hl`, `gl`, `ceid`), Decision 4: Resolução de URLs do Google News via `googlenewsdecoder`, Decision 5: Logging em Tempo Real no `stderr` e Segregação de Streams, Research: Google News Headlines Extractor
### Community 92 - "General Readiness Checklist: Google News Headlines Extractor"
Cohesion: 0.29
Nodes (7): CLI Interface & Parameter Contracts, Data Sanitization & Article Extraction, Error Handling & Edge Cases, General Readiness Checklist: Google News Headlines Extractor, Non-Functional & Operational Readiness, Notes, Scraping Engine & Feed Mapping
### Community 93 - "1. Entidades de Domínio & DTOs"
Cohesion: 0.29
Nodes (6): 1.1 SearchQuery (Parâmetros da Busca), 1.2 NewsArticle (Item de Notícia), 1.3 ExtractionResult (Saída Estruturada Consolidada), 1. Entidades de Domínio & DTOs, 2. Esquema JSON de Saída, Data Model: Google News Headlines Extractor
### Community 94 - "Specification Quality Checklist: Google News Headlines Extractor"
Cohesion: 0.33
Nodes (5): Content Quality, Feature Readiness, Notes, Requirement Completeness, Specification Quality Checklist: Google News Headlines Extractor
### Community 95 - "CLI Contract: Google News Headlines Extractor"
Cohesion: 0.33
Nodes (6): 1. Comando e Argumentos, 2. Códigos de Saída (Exit Codes), 3. Protocolo de Streams (Stdout / Stderr), Argumentos de Linha de Comando, CLI Contract: Google News Headlines Extractor, Sintaxe
### Community 97 - "🧠 TextNLPClassifierApp"
Cohesion: 0.04
Nodes (45): 1. 🧠 Classificador de Conteúdo e Inerência (NLP / LLM / ECP), 1. Clonar o Repositório e Criar Ambiente Virtual, 1. Conversão Padrão, 1. Execução Padrão Automática, 2. Conversão com Caminho de Destino Personalizado, 2. Execução com Modo Verboso, 2. 📰 Extrator de Manchetes do Google News, 2. Instalar Dependências (+37 more)
### Community 98 - "Extraction Pipeline Checklist: Article Content Multi-Engine Extractor"
Cohesion: 0.05
Nodes (34): 1. Requirement Completeness, 2. Requirement Clarity & Non-Ambiguity, 3. Requirement Consistency & Data Contracts, 4. Scenario & Edge Case Coverage, 5. Non-Functional & Operational Readiness, Extraction Pipeline Checklist: Article Content Multi-Engine Extractor, Notes, Content Quality (+26 more)
### Community 99 - "parametrize"
Cohesion: 0.22
Nodes (9): parametrize, Garante aceitação de URLs absolutas com esquema HTTP e HTTPS válidos., Garante rejeição de esquemas não permitidos, URLs relativas e strings vazias., Garante que a ausência de corpo no extrator selecionado NUNCA faça fallback…, Garante que todos os placeholders documentados no PRD sejam descartados…, test_normalize_scalar_placeholders_discarded(), test_resolve_article_body_strict_isolation_all_extractors(), test_validate_url_invalid_schemes() (+1 more)
### Community 100 - "main"
Cohesion: 0.14
Nodes (20): main(), Path, Cenário 6.1: Caminho de ECP inexistente -> Exit Code 1, error_code:…, Cenário 6.2: Arquivo ECP com sintaxe JSON corrompida., Cenário 6.3: Valida erro para falta de cada um dos campos obrigatórios do ECP., Cenário 6.4: Caminho de arquivo Markdown inexistente., Cenário 6.5: Arquivo Markdown vazio ou contendo apenas espaços em branco., Cenário 6.6: A flag -o / --output cria diretórios aninhados automaticamente. (+12 more)
### Community 101 - "classifier.py"
Cohesion: 0.17
Nodes (12): emit_error(), parse_args(), Namespace, Core deterministic classification engine (Tier 1 core)., ErrorCode, MatchedGraphEntity, Enum, str (+4 more)
### Community 102 - "Feature Specification: Multilingual NLP Entity Inherence Classifier (POC)"
Cohesion: 0.14
Nodes (14): Assumptions, Assumptions & Scope, Clarifications, Explicit Out of Scope (POC), Feature Specification: Multilingual NLP Entity Inherence Classifier (POC), Functional Requirements, Key Entities *(data models & domain entities)*, Measurable Outcomes (+6 more)
### Community 103 - "4. Requisitos Funcionais (FR)"
Cohesion: 0.10
Nodes (20): 1.1 Objetivo do Produto, 1. Visão Geral e Contexto, 2. Personas e Casos de Uso, 3. Arquitetura e Fluxo do Sistema, 4. Requisitos Funcionais (FR), 5. Requisitos Não Funcionais (NFR), 6.1 Esquema do JSON de Entrada (`Input`), 6.2 Esquema do JSON Consolidado de Saída (`Output`) (+12 more)
### Community 104 - "Tasks: Article Content Multi-Engine Extractor"
Cohesion: 0.11
Nodes (18): Dependencies & Execution Order, Entrega Incremental, Implementation Strategy, Implementação da User Story 1, Implementação da User Story 2, Implementação da User Story 3, MVP First (User Story 1 Only), Oportunidades de Execução Paralela (+10 more)
### Community 105 - "Tasks: Convert Article JSON to Markdown"
Cohesion: 0.11
Nodes (19): Dependencies & Execution Order, Implementation for User Story 1, Implementation for User Story 2, Implementation for User Story 3, Implementation Strategy, Incremental Delivery, MVP First (User Story 1 Only), Parallel Opportunities (+11 more)
### Community 106 - "Implementation Plan: Article Content Multi-Engine Extractor"
Cohesion: 0.17
Nodes (11): Constitution Check, Documentation (this feature), Implementation Phases, Implementation Plan: Article Content Multi-Engine Extractor, Phase 0: Outline & Research *(Completed)*, Phase 1: Design & Contracts *(Completed)*, Phase 2: Tasks & Execution *(Completed)*, Project Structure (+3 more)
### Community 107 - "2. Cenários de Validação"
Cohesion: 0.22
Nodes (8): 1. Pré-requisitos, 2. Cenários de Validação, 3. Validação Automatizada de Testes, Cenário 1: Extração com Amostragem Rápida (Limit 2), Cenário 2: Caminho Customizado de Saída, Cenário 3: Modo Silencioso (`--silent`), Cenário 4: Resiliência contra URLs Inválidas, Quickstart & Validation Guide: Article Content Multi-Engine Extractor
### Community 108 - "1. Technical Decisions & Tradeoffs"
Cohesion: 0.22
Nodes (8): 1. Technical Decisions & Tradeoffs, Decision 1: Motor de Navegação e Renderização com `foxcape` em Sessão Única, Decision 2: Orquestração Tripla de Extração de Conteúdo (NLP & Web Scraping), Decision 3: Resiliência e Isolamento de Falhas por Camada, Decision 4: Herança Inteligente de Idioma para NLP, Decision 5: Gerenciamento de Memória e Descarte do Raw HTML, Decision 6: Segregação de Streams e Feedback Visual em `stderr`, Research: Article Content Multi-Engine Extractor
### Community 109 - "Implementation Plan: Multilingual NLP Entity Inherence Classifier (POC)"
Cohesion: 0.25
Nodes (8): Complexity Tracking, Constitution Check, Documentation (this feature), Implementation Plan: Multilingual NLP Entity Inherence Classifier (POC), Project Structure, Source Code (repository root), Summary, Technical Context
### Community 110 - "POC Readiness & Requirements Quality Checklist: Multilingual NLP Entity Inherence Classifier"
Cohesion: 0.29
Nodes (7): 1. Requirement Completeness & Scope Boundaries, 2. Requirement Clarity & Decision Semantics, 3. Requirement Consistency & Alignment, 4. Acceptance Criteria & Measurability, 5. Scenario & Edge Case Coverage, Notes, POC Readiness & Requirements Quality Checklist: Multilingual NLP Entity Inherence Classifier
### Community 111 - "Specification Quality Checklist: Multilingual NLP Entity Inherence Classifier"
Cohesion: 0.33
Nodes (5): Content Quality, Feature Readiness, Notes, Requirement Completeness, Specification Quality Checklist: Multilingual NLP Entity Inherence Classifier
### Community 112 - "CLI Contract: Article Content Multi-Engine Extractor"
Cohesion: 0.33
Nodes (5): 1. Comando de Execução, 2. Argumentos e Flags, 3. Códigos de Saída (Exit Codes), 4. Comportamento de Streams (I/O), CLI Contract: Article Content Multi-Engine Extractor
### Community 114 - "JSON Schema Contract: Article Content Multi-Engine Extractor"
Cohesion: 0.50
Nodes (3): 1. Schema de Entrada (Input JSON), 2. Schema de Saída (Output JSON), JSON Schema Contract: Article Content Multi-Engine Extractor
### Community 115 - "PRD — Seleção determinística da biblioteca de extração de conteúdo"
Cohesion: 0.07
Nodes (29): 10. Requisitos não funcionais, 11. Critérios de aceite, 12. Casos obrigatórios de teste, 13. Definition of Done, 1. Contexto, 2. Objetivo, 3.1 Incluído, 3.2 Fora do escopo (+21 more)
### Community 116 - "select_article_extractor.py"
Cohesion: 0.14
Nodes (19): ArticleSelectionResult, BatchProcessingResult, break_priority_tie(), calculate_consensus_metrics(), ExtractorCandidate, form_active_set(), main(), parse_args() (+11 more)
### Community 117 - "1. Text Normalization Pipeline"
Cohesion: 0.11
Nodes (18): 1. Text Normalization Pipeline, 2. 5-Token Shingles & Consensus Metrics, 3. Regras de Decisão, Empate Técnico e Desempate Hierárquico, 4. Estratégia de I/O Não Destrutiva e Escrita Atômica, Alternatives Considered, Context, Context, Context (+10 more)
### Community 118 - "Tasks: Deterministic Article Content Selection"
Cohesion: 0.11
Nodes (18): Dependencies & Execution Order, Implementation for User Story 1, Implementation for User Story 2, Implementation for User Story 3, Implementation Strategy, Incremental Delivery (Phases 4, 5 & 6), MVP First (Phases 1, 2 & 3), Parallel Opportunities (+10 more)
### Community 119 - "select_article_extractor"
Cohesion: 0.08
Nodes (35): CandidateStatus, extract_candidate_data(), ExtractorName, Any, Enum, str, Extrai campo de texto, erro e calcula tokens/shingles para um motor., Nomes canônicos e catálogo fechado dos motores de extração. (+27 more)
### Community 120 - "process_batch"
Cohesion: 0.11
Nodes (24): atomic_save_json(), process_batch(), Path, Salva dados em JSON de forma atômica utilizando arquivo temporário e rename., Lê o JSON de entrada, valida a estrutura, processa todos os artigos e grava o…, Path, CT-012: A entrada já contém selected_extractor -> Recalcular e substituir…, CT-013: articles está vazio -> Gerar saída válida com articles vazio. (+16 more)
### Community 121 - "test_models.py"
Cohesion: 0.07
Nodes (37): count_phrase_occurrences(), match_phrase_in_text(), Check if a normalized phrase appears in normalized text with word boundary…, Count occurrences of a phrase in text., Classify inherence of content against an ECP snapshot., detect_language(), extract_words(), normalize_text() (+29 more)
### Community 122 - "test_select_article_extractor.py"
Cohesion: 0.18
Nodes (16): generate_shingles(), normalize_text(), Executa a normalização determinística para comparação: 1. Decodificar entidades…, Gera conjunto de shingles ordenados de tamanho window_size (padrão 5). - Se…, Suíte de Testes Automatizados para o Seletor Determinístico de Extrator. Cobre…, Garante que marcação de imagem Markdown ![alt](url) seja descartada e link…, E2E: Executa scripts/select_article_extractor.py como subprocesso real na linha…, test_e2e_cli_subprocess_real_execution() (+8 more)
### Community 123 - "Feature Specification: Deterministic Content Selection"
Cohesion: 0.17
Nodes (12): Assumptions, Edge Cases, Feature Specification: Deterministic Content Selection, Functional Requirements, Key Entities *(include if feature involves data)*, Measurable Outcomes, Requirements *(mandatory)*, Success Criteria *(mandatory)* (+4 more)
### Community 124 - "2. Entity Descriptions & Fields"
Cohesion: 0.18
Nodes (11): 1. Domain Entities & Value Types, 2. Entity Descriptions & Fields, 3. JSON Schema Mapping, `ArticleSelectionResult` (Dataclass), `BatchProcessingResult` (Dataclass), `CandidateStatus` (Enum), Data Model: Deterministic Content Selection, Entrada (+3 more)
### Community 125 - "Implementation Plan: Deterministic Article Content Selection"
Cohesion: 0.18
Nodes (11): Constitution Check, Documentation (this feature), Implementation Phases, Implementation Plan: Deterministic Article Content Selection, Phase 0: Outline & Research *(Completed)*, Phase 1: Design & Contracts *(Completed)*, Phase 2: Tasks & Execution *(Next Step via `/speckit-tasks`)*, Project Structure (+3 more)
### Community 126 - "Deterministic Content Selection Checklist: End-to-End Requirements Quality"
Cohesion: 0.25
Nodes (7): Candidate State Transitions & Resilience, Decision & Tie-Breaking Hierarchy, Deterministic Content Selection Checklist: End-to-End Requirements Quality, JSON Schema Integrity & Atomic I/O, Notes, Shingles & Consensus Metric Formulation, Text Normalization & Tokenization Quality
### Community 127 - "Quickstart: Deterministic Article Content Selection"
Cohesion: 0.29
Nodes (7): 1. Pré-requisitos, 2. Execução Rápida via CLI, 3. Execução dos Testes Automatizados, 4. Validação Programática / Uso como Módulo Python, Cenário 1: Selecionar o melhor extrator para uma extração existente, Cenário 2: Especificar caminho de saída customizado e modo verboso, Quickstart: Deterministic Article Content Selection
### Community 128 - "Specification Quality Checklist: Deterministic Content Selection"
Cohesion: 0.33
Nodes (5): Content Quality, Feature Readiness, Notes, Requirement Completeness, Specification Quality Checklist: Deterministic Content Selection
### Community 129 - "CLI Interface Contract: Deterministic Article Content Selection"
Cohesion: 0.33
Nodes (5): 1. Command Syntax, 2. Arguments and Flags, 3. Standard Streams (I/O), 4. Exit Codes, CLI Interface Contract: Deterministic Article Content Selection
### Community 131 - "convert_article_to_markdown.py"
Cohesion: 0.19
Nodes (17): _get_dict(), normalize_date(), normalize_list(), normalize_scalar(), Any, Interpreta datas ISO 8601 e RFC 2822 preservando fuso horário ou YYYY-MM-DD…, Valida se a URL é absoluta com protocolo http ou https e hostname não vazio., Retorna o dicionário associado à chave ou um dicionário vazio caso não seja… (+9 more)
### Community 132 - "8. Regras funcionais"
Cohesion: 0.13
Nodes (15): 8. Regras funcionais, RF-001 — Receber um único artigo, RF-002 — Respeitar o extrator selecionado, RF-003 — Resolver o corpo dentro do extrator selecionado, RF-004 — Selecionar metadados deterministicamente, RF-005 — Priorizar metadados por campo, RF-006 — Normalizar valores escalares, RF-007 — Normalizar listas (+7 more)
### Community 133 - "12. Critérios de aceite"
Cohesion: 0.14
Nodes (14): 12. Critérios de aceite, CA-001 — Trafilatura selecionada, CA-002 — Newspaper4k selecionado, CA-003 — Readability selecionado, CA-004 — Fallback dentro do extrator, CA-005 — Proibição de fallback de corpo entre extratores, CA-006 — Metadado vindo de outro extrator, CA-007 — Obrigatórios presentes (+6 more)
### Community 134 - "PRD — Conversão de artigo JSON para Markdown"
Cohesion: 0.17
Nodes (11): 10. Tratamento de erros, 14. Definition of Done, 15. Dependência técnica escolhida, 1. Visão geral, 2. Problema, 3. Objetivo, 4. História do usuário, 7.1 Arquivo (+3 more)
### Community 135 - "resolve_article_body"
Cohesion: 0.20
Nodes (10): Obtém o corpo do artigo exclusivamente do selected_extractor com fallback…, resolve_article_body(), Testa prioridade trafilatura.markdown sobre trafilatura.text., Testa prioridade newspaper4k.article_html sobre newspaper4k.text., Testa prioridade readability.cleaned_html sobre readability.cleaned_text., Garante erro ao receber selected_extractor ausente ou não reconhecido., test_resolve_article_body_invalid_selected_extractor(), test_resolve_article_body_newspaper4k_primary_and_fallback() (+2 more)
### Community 136 - "Implementation Plan: Convert Article JSON to Markdown"
Cohesion: 0.17
Nodes (12): Complexity Tracking, Constitution Check, Documentation (this feature), Implementation Phases, Implementation Plan: Convert Article JSON to Markdown, Phase 0: Outline & Research *(Completed)*, Phase 1: Design & Contracts *(Completed)*, Phase 2: Tasks & Implementation Breakdown *(Next: `/speckit-tasks`)* (+4 more)
### Community 137 - "2. Technical Decisions & Research Findings"
Cohesion: 0.17
Nodes (11): 1. Executive Summary & Goals, 2. Technical Decisions & Research Findings, 3. Technology Stack & Dependencies, Decision 1: HTML-to-Markdown Engine Selection, Decision 2: Direct Markdown Handling for Trafilatura, Decision 3: Metadata Normalization & Priority Resolution Pipeline, Decision 4: Date Parsing Strategy (ISO 8601 & RFC 2822), Decision 5: URL Validation & Media Filtering (+3 more)
### Community 138 - "Feature Specification: Convert Article JSON to Markdown"
Cohesion: 0.17
Nodes (12): Assumptions, Edge Cases, Feature Specification: Convert Article JSON to Markdown, Functional Requirements, Key Entities, Measurable Outcomes, Requirements *(mandatory)*, Success Criteria *(mandatory)* (+4 more)
### Community 139 - "Markdown Conversion Checklist: End-to-End Requirements Quality"
Cohesion: 0.18
Nodes (11): 1. Validação de Entrada, Tipagem & Isolamento de Lotes, 2. Isolamento Estrito de Extrator & Conversão de Conteúdo (HTML/MD), 3. Resolução Determinística de Metadados & Mapeamento de SELECIONADO, 4. Normalização de Escalares, Placeholders & Sanitização de Listas, 5. Normalização de Datas, Fusos & Validação Estrita de URLs, 6. Sanitização Editorial, Tratamento de Imagens & Título Duplicado, 7. Estrutura, Sintaxe do Markdown de Saída & Restrições, 8. Interface CLI, Tratamento de Erros & Atomicidade (+3 more)
### Community 140 - "convert_article"
Cohesion: 0.15
Nodes (13): assemble_markdown_document(), clean_body_images(), convert_article(), Path, Preserva imagens com URL absoluta http/https, remove relativas/data:/vazias e…, Monta a estrutura final do documento Markdown respeitando a ordem estrita do…, Executa a leitura do JSON, validação, conversão e escrita atômica do arquivo…, Valida descarte de data:, relativos e deduplicação mantendo a primeira… (+5 more)
### Community 141 - "005-convert-json-markdown/plan.md"
Cohesion: 0.33
Nodes (3): 1. Output Document Specification, 2. Formatting & Syntax Constraints, Markdown Schema Contract: Output Article Markdown
### Community 142 - "Quickstart: Convert Article JSON to Markdown"
Cohesion: 0.22
Nodes (8): 1. Prerequisites & Setup, 2. Running the CLI Tool, 3. Verification & Testing, Basic Conversion (Default Output Path), Custom Destination Path, Quickstart: Convert Article JSON to Markdown, Run All Unit & Integration Tests, Run Linter & Type Checker
### Community 143 - "1. Domain Entities & Schemas"
Cohesion: 0.25
Nodes (8): 1. Domain Entities & Schemas, 2. Priority Resolution Matrix, Data Model: Convert Article JSON to Markdown, Entity 1: `ArticleInput` (Source JSON), Entity 2: `ExtractorBlock` (Per-Extractor Data), Entity 3: `ResolvedArticleMetadata`, Entity 4: `MarkdownDocument`, Validation Rules:
### Community 144 - "11. Requisitos não funcionais"
Cohesion: 0.29
Nodes (7): 11. Requisitos não funcionais, RNF-001 — Determinismo, RNF-002 — Compatibilidade, RNF-003 — Execução local, RNF-004 — Integridade, RNF-005 — Manutenibilidade, RNF-006 — Qualidade
### Community 145 - "parse_arguments"
Cohesion: 0.29
Nodes (7): main(), parse_arguments(), Namespace, Configura o parser de argumentos do CLI., Ponto de entrada do CLI., Testa a chamada direta do parse_arguments., test_parse_arguments_api_direct()
### Community 146 - "Specification Quality Checklist: Convert Article JSON to Markdown"
Cohesion: 0.33
Nodes (5): Content Quality, Feature Readiness, Notes, Requirement Completeness, Specification Quality Checklist: Convert Article JSON to Markdown
### Community 147 - "CLI Contract: `convert_article_to_markdown.py`"
Cohesion: 0.33
Nodes (5): 1. Script Signature, 2. Command-Line Arguments, 3. Exit Codes, 4. Standard Stream Behavior, CLI Contract: `convert_article_to_markdown.py`
### Community 148 - "9. Interface CLI"
Cohesion: 0.40
Nodes (5): 9.1 Script, 9.2 Argumentos, 9.3 Exemplos, 9.4 Saída do processo, 9. Interface CLI
### Community 149 - "sample_rss_xml"
Cohesion: 0.67
Nodes (3): fixture, Fixture que fornece o conteúdo do XML de exemplo para testes offline., sample_rss_xml()
### Community 150 - "13. Estratégia de testes"
Cohesion: 0.50
Nodes (4): 13.1 Testes unitários, 13.2 Testes de integração do CLI, 13.3 Casos de resultado esperado, 13. Estratégia de testes
### Community 151 - "6. Contrato de entrada"
Cohesion: 0.50
Nodes (4): 6.1 Formato, 6.2 Valores aceitos para `selected_extractor`, 6.3 Campos obrigatórios após a resolução, 6. Contrato de entrada
### Community 152 - "ECPSnapshot"
Cohesion: 0.13
Nodes (20): ECPSnapshot, parametrize, test_benchmark_case(), Suíte de Testes para o Adaptador de Fallback para LLM (Tier 3) do Classificador…, Valida extração de JSON quando a resposta do LLM vem formatada em bloco…, Valida que respostas corrompidas ou JSONs sem campos obrigatórios retornem None…, Garante que o classificador dispare o Tier 3 LLM para casos ambíguos…, Garante que casos claros (alta confiança e alta densidade de âncoras) NÃO… (+12 more)
### Community 153 - "convert_html_to_markdown"
Cohesion: 0.33
Nodes (6): convert_html_to_markdown(), Converte HTML para Markdown usando títulos ATX, removendo scripts e estilos., Valida conversão de elementos HTML estruturados para Markdown com títulos ATX., Testa conversão de HTML vazio retornando string vazia., test_convert_html_to_markdown_empty_or_whitespace(), test_convert_html_to_markdown_rich_formatting()
### Community 154 - "JSON Schema Contract: Deterministic Article Content Selection"
Cohesion: 0.50
Nodes (3): 1. Input JSON Schema, 2. Output JSON Schema, JSON Schema Contract: Deterministic Article Content Selection
### Community 155 - "5. Escopo"
Cohesion: 0.67
Nodes (3): 5.1 Incluído, 5.2 Fora do escopo, 5. Escopo
### Community 165 - "InherenceClassifier"
Cohesion: 0.07
Nodes (45): InherenceClassifier, Tier 1 Deterministic NLP Entity Inherence Classifier with optional Tier 2 /…, DecisionCategory, Content about apple fruit/culinary recipe against Apple Inc. tech entity., test_adversarial_apple_fruit_recipe(), Unit tests for deterministic classification decision logic., test_contextual_inherent(), test_direct_inherent() (+37 more)
### Community 166 - "remove_duplicate_initial_h1"
Cohesion: 0.50
Nodes (4): Remove o primeiro título H1 do corpo somente quando ele for igual ao título…, remove_duplicate_initial_h1(), Testa remoção de H1 inicial coincidente com título com variações de espaços e…, test_remove_duplicate_initial_h1_exact_and_variations()
### Community 168 - ".disambiguate"
Cohesion: 0.25
Nodes (4): Executes LLM fallback for ambiguous boundary cases. Returns a refined…, Parses and validates structured JSON response from LLM., Returns True if an API key or custom provider function is configured., Constructs an expert-engineered prompt for multilingual entity inherence…
### Community 169 - "test_funnel_cli_subprocess_end_to_end"
Cohesion: 0.67
Nodes (3): Path, Valida o contrato CLI completo classify.py com saída em arquivo JSON e flags…, test_funnel_cli_subprocess_end_to_end()
## Knowledge Gaps
- **696 isolated node(s):** `text-nlp-classifier`, `MatchedGraphEntity`, `graphify`, `Usage`, `What graphify is for` (+691 more)
These have ≤1 connection - possible missing edges or undocumented components.
- **48 thin communities (<3 nodes) omitted from report** — run `graphify query` to explore isolated nodes.
## Suggested Questions
_Questions this graph is uniquely positioned to answer:_
- **Why does `ECPSnapshot` connect `ECPSnapshot` to `main`, `classifier.py`, `InherenceClassifier`, `.disambiguate`, `ClassificationResult`, `test_adversarial.py`, `LLMFallbackAdapter`, `test_models.py`?**
_High betweenness centrality (0.009) - this node is a cross-community bridge._
- **Why does `PRD — Conversão de artigo JSON para Markdown` connect `PRD — Conversão de artigo JSON para Markdown` to `8. Regras funcionais`, `12. Critérios de aceite`, `11. Requisitos não funcionais`, `9. Interface CLI`, `13. Estratégia de testes`, `6. Contrato de entrada`, `5. Escopo`?**
_High betweenness centrality (0.004) - this node is a cross-community bridge._
- **Why does `InherenceClassifier` connect `InherenceClassifier` to `main`, `classifier.py`, `ClassificationResult`, `test_adversarial.py`, `LLMFallbackAdapter`, `ECPSnapshot`, `test_models.py`?**
_High betweenness centrality (0.004) - this node is a cross-community bridge._
- **Are the 42 inferred relationships involving `ECPSnapshot` (e.g. with `main()` and `BaseNLPAdapter`) actually correct?**
_`ECPSnapshot` has 42 INFERRED edges - model-reasoned connections that need verification._
- **Are the 8 inferred relationships involving `InherenceClassifier` (e.g. with `LocalEmbeddingsAdapter` and `LLMFallbackAdapter`) actually correct?**
_`InherenceClassifier` has 8 INFERRED edges - model-reasoned connections that need verification._
- **Are the 50 inferred relationships involving `DecisionCategory` (e.g. with `LLMFallbackAdapter` and `InherenceClassifier`) actually correct?**
_`DecisionCategory` has 50 INFERRED edges - model-reasoned connections that need verification._
- **Are the 4 inferred relationships involving `LLMFallbackAdapter` (e.g. with `ClassificationResult` and `DecisionCategory`) actually correct?**
_`LLMFallbackAdapter` has 4 INFERRED edges - model-reasoned connections that need verification._
File diff suppressed because it is too large Load Diff
+932
View File
@@ -0,0 +1,932 @@
{
".specify/scripts/powershell/check-prerequisites.ps1": {
"mtime": 1787189272.2023797,
"seen": 1787189326.0933716,
"ast_hash": "ab9d4c9d5772d28c6b2150b1f53ac638",
"semantic_hash": ""
},
".specify/scripts/powershell/common.ps1": {
"mtime": 1787189272.20638,
"seen": 1787189326.0933838,
"ast_hash": "6d1dc2322173d3cd13a3633d61324293",
"semantic_hash": ""
},
".specify/scripts/powershell/create-new-feature.ps1": {
"mtime": 1787189272.211369,
"seen": 1787189326.0933893,
"ast_hash": "fad051375e6dcd47030cc49188472fe1",
"semantic_hash": ""
},
".specify/scripts/powershell/resolve-template.ps1": {
"mtime": 1787189272.2143798,
"seen": 1787189326.0933924,
"ast_hash": "645ad018c6297df6e23949ac3e915749",
"semantic_hash": ""
},
".specify/scripts/powershell/setup-plan.ps1": {
"mtime": 1787189272.2183795,
"seen": 1787189326.0933957,
"ast_hash": "02abf0794256a7423782adc760f0e0ca",
"semantic_hash": ""
},
".specify/scripts/powershell/setup-tasks.ps1": {
"mtime": 1787189272.22138,
"seen": 1787189326.093398,
"ast_hash": "9aa2f52ce7abece8476845b1f21a0ced",
"semantic_hash": ""
},
".agents/skills/graphify/SKILL.md": {
"mtime": 1787189375.57439,
"seen": 1787189383.8494325,
"ast_hash": "1656a8a8e4a05bbe69f32cd15de81e87",
"semantic_hash": ""
},
".agents/skills/graphify/references/add-watch.md": {
"mtime": 1787176247.1083012,
"seen": 1787189352.7428358,
"ast_hash": "d59d027fe449f06aa03905a01184e779",
"semantic_hash": ""
},
".agents/skills/graphify/references/exports.md": {
"mtime": 1787176247.109326,
"seen": 1787189352.742837,
"ast_hash": "9da2a2152e60a22f07f05fed5158f287",
"semantic_hash": ""
},
".agents/skills/graphify/references/extraction-spec.md": {
"mtime": 1787176247.109326,
"seen": 1787189352.7428386,
"ast_hash": "42fef56a01698118f7fd722acdadea34",
"semantic_hash": ""
},
".agents/skills/graphify/references/github-and-merge.md": {
"mtime": 1787176247.1118312,
"seen": 1787189352.7428403,
"ast_hash": "cde13df085e4c492aeb7ba298a996d0d",
"semantic_hash": ""
},
".agents/skills/graphify/references/hooks.md": {
"mtime": 1787176247.1128435,
"seen": 1787189352.7428415,
"ast_hash": "3f545db6ce646650bb9de588f5512a27",
"semantic_hash": ""
},
".agents/skills/graphify/references/query.md": {
"mtime": 1787176247.1138976,
"seen": 1787189352.742843,
"ast_hash": "3c46f8ad006874260614ed8657d02307",
"semantic_hash": ""
},
".agents/skills/graphify/references/transcribe.md": {
"mtime": 1787176247.1138976,
"seen": 1787189352.7428443,
"ast_hash": "67b6a5fa6c0974e18f60db9f8932fbd8",
"semantic_hash": ""
},
".agents/skills/graphify/references/update.md": {
"mtime": 1787176247.11542,
"seen": 1787189352.7428463,
"ast_hash": "9378bd03d56baaf78bd738ab3848f142",
"semantic_hash": ""
},
".agents/skills/ponytail-audit/SKILL.md": {
"mtime": 1786995804.1528327,
"seen": 1787189326.0934196,
"ast_hash": "741076540e91247a0bace7b89c27ac6b",
"semantic_hash": ""
},
".agents/skills/ponytail-debt/SKILL.md": {
"mtime": 1786995804.1528327,
"seen": 1787189326.0934212,
"ast_hash": "19c1eef33c9b109abb5805a762a545c8",
"semantic_hash": ""
},
".agents/skills/ponytail-gain/SKILL.md": {
"mtime": 1786995804.1542277,
"seen": 1787189326.0934231,
"ast_hash": "8f9cd980326238785d2fc23a9cfddc96",
"semantic_hash": ""
},
".agents/skills/ponytail-help/SKILL.md": {
"mtime": 1786995804.1542277,
"seen": 1787189326.0934246,
"ast_hash": "25b3214341fdc2f0805d2f8a5da6fefd",
"semantic_hash": ""
},
".agents/skills/ponytail-review/SKILL.md": {
"mtime": 1786995804.1552386,
"seen": 1787189326.0934267,
"ast_hash": "52f93e973047baa9a906c6dc5879a4a6",
"semantic_hash": ""
},
".agents/skills/ponytail/SKILL.md": {
"mtime": 1786995804.1562397,
"seen": 1787189326.0934284,
"ast_hash": "33f013ce03c8c66a6002ef941a433b81",
"semantic_hash": ""
},
".agents/skills/speckit-analyze/SKILL.md": {
"mtime": 1787189272.133064,
"seen": 1787189326.0934305,
"ast_hash": "d0496db1f414b10bb0c8d57d5009f378",
"semantic_hash": ""
},
".agents/skills/speckit-checklist/SKILL.md": {
"mtime": 1787189272.1603796,
"seen": 1787189326.0934336,
"ast_hash": "94a6ee97b010344e446c25798ad7779b",
"semantic_hash": ""
},
".agents/skills/speckit-clarify/SKILL.md": {
"mtime": 1787189272.137533,
"seen": 1787189326.0934358,
"ast_hash": "9ed49f88546289c1f40df65c485ace1a",
"semantic_hash": ""
},
".agents/skills/speckit-constitution/SKILL.md": {
"mtime": 1787189272.1413815,
"seen": 1787189326.0934374,
"ast_hash": "146185078e43d2e95014fda5b36f7461",
"semantic_hash": ""
},
".agents/skills/speckit-converge/SKILL.md": {
"mtime": 1787189272.1503813,
"seen": 1787189326.0934393,
"ast_hash": "6857bd3f30fc0c52352a450a8c2c0591",
"semantic_hash": ""
},
".agents/skills/speckit-implement/SKILL.md": {
"mtime": 1787189272.146373,
"seen": 1787189326.0934412,
"ast_hash": "562d500cddb51ca6d5f23971f7539341",
"semantic_hash": ""
},
".agents/skills/speckit-plan/SKILL.md": {
"mtime": 1787189272.15438,
"seen": 1787189326.0934432,
"ast_hash": "aba2d03c1b88c8f928ac22f7e3d75a25",
"semantic_hash": ""
},
".agents/skills/speckit-specify/SKILL.md": {
"mtime": 1787189272.165381,
"seen": 1787189326.093445,
"ast_hash": "a9dd8232755de1ac099f67ae212978b9",
"semantic_hash": ""
},
".agents/skills/speckit-tasks/SKILL.md": {
"mtime": 1787189272.1693797,
"seen": 1787189326.0934472,
"ast_hash": "faba87885d7a9309af89e1ad27d08e10",
"semantic_hash": ""
},
".agents/skills/speckit-taskstoissues/SKILL.md": {
"mtime": 1787189272.172365,
"seen": 1787189326.093449,
"ast_hash": "b99619d8bd31aac390a0ff96943d165c",
"semantic_hash": ""
},
".specify/memory/constitution.md": {
"mtime": 1787189272.25438,
"seen": 1787189326.0934513,
"ast_hash": "dcefb6b60e7cb3db2e966977b6f02f18",
"semantic_hash": ""
},
".specify/templates/checklist-template.md": {
"mtime": 1787189272.2233796,
"seen": 1787189326.0934534,
"ast_hash": "5af23404f7e1314e93fab2bcfcfa5c23",
"semantic_hash": ""
},
".specify/templates/constitution-template.md": {
"mtime": 1787189272.2263799,
"seen": 1787189326.093455,
"ast_hash": "dcefb6b60e7cb3db2e966977b6f02f18",
"semantic_hash": ""
},
".specify/templates/plan-template.md": {
"mtime": 1787189272.2293687,
"seen": 1787189326.0934572,
"ast_hash": "311db0670c3d19d2f5b781de97529b79",
"semantic_hash": ""
},
".specify/templates/spec-template.md": {
"mtime": 1787189272.2313685,
"seen": 1787189326.0934594,
"ast_hash": "45ac8538bc1220324c9f0d610145b53f",
"semantic_hash": ""
},
".specify/templates/tasks-template.md": {
"mtime": 1787189272.2343798,
"seen": 1787189326.0934615,
"ast_hash": "f8b4f62d384defa0a559bdbbaacb0540",
"semantic_hash": ""
},
".specify/workflows/speckit/workflow.yml": {
"mtime": 1787160407.4626267,
"seen": 1787189326.0934637,
"ast_hash": "06fd89e478bb8e0e668917a5f02f9055",
"semantic_hash": ""
},
".agents/rules/graphify.md": {
"mtime": 1787189347.4441817,
"seen": 1787189352.7428298,
"ast_hash": "a0e4d192b9e2e17256124dbfc8e24e6a",
"semantic_hash": ""
},
".agents/workflows/graphify.md": {
"mtime": 1787189347.4441817,
"seen": 1787189352.7449548,
"ast_hash": "a831cf3669f1e6ab85d8a80ebe655f99",
"semantic_hash": ""
},
"specs/001-multilingual-entity-classifier/checklists/requirements.md": {
"mtime": 1787193985.655018,
"seen": 1787194003.0704188,
"ast_hash": "13e007d6da3f275d34b1f67fda546b81",
"semantic_hash": ""
},
"specs/001-multilingual-entity-classifier/spec.md": {
"mtime": 1787194850.5106893,
"seen": 1787194931.113662,
"ast_hash": "41587427964956662ffb8f0dffea1027",
"semantic_hash": ""
},
"specs/001-multilingual-entity-classifier/contracts/cli-contract.md": {
"mtime": 1787194605.824187,
"seen": 1787194638.8537047,
"ast_hash": "c4fdb88a5e3269fb10689a2940b9616f",
"semantic_hash": ""
},
"specs/001-multilingual-entity-classifier/data-model.md": {
"mtime": 1787194591.677816,
"seen": 1787194638.853709,
"ast_hash": "7ccdd584024c65f16364444e9df4d860",
"semantic_hash": ""
},
"specs/001-multilingual-entity-classifier/plan.md": {
"mtime": 1787194897.8541067,
"seen": 1787194931.1135547,
"ast_hash": "edcaea62d445dbb073ff30166115788e",
"semantic_hash": ""
},
"specs/001-multilingual-entity-classifier/quickstart.md": {
"mtime": 1787194877.439851,
"seen": 1787194931.113557,
"ast_hash": "d630b7592e1ee761f1d36cfd0079771d",
"semantic_hash": ""
},
"specs/001-multilingual-entity-classifier/research.md": {
"mtime": 1787194581.004019,
"seen": 1787194638.8537128,
"ast_hash": "159b7eec565d477f15f2c683c082e40d",
"semantic_hash": ""
},
"specs/001-multilingual-entity-classifier/tasks.md": {
"mtime": 1787196452.595599,
"seen": 1787196463.1030743,
"ast_hash": "132f38dcd33d45eec9a8ca22ec4de880",
"semantic_hash": ""
},
"specs/001-multilingual-entity-classifier/checklists/poc-readiness.md": {
"mtime": 1787195218.2475252,
"seen": 1787195225.042909,
"ast_hash": "f8488016a8e749938d2086d34999a91c",
"semantic_hash": ""
},
"classify.py": {
"mtime": 1787320064.0177462,
"seen": 1787320239.022448,
"ast_hash": "3679326c88a59f796f587e0c61c31417",
"semantic_hash": ""
},
"pyproject.toml": {
"mtime": 1787264482.8509245,
"seen": 1787264519.0539448,
"ast_hash": "409c1fc0d7bbda6830ccb71c6196cb2f",
"semantic_hash": ""
},
"src/__init__.py": {
"mtime": 1787195832.559486,
"seen": 1787196463.0953057,
"ast_hash": "13284e9b9f10de535dba5f461ecd7c7c",
"semantic_hash": ""
},
"src/adapters/__init__.py": {
"mtime": 1787195842.2716768,
"seen": 1787196463.0953088,
"ast_hash": "541577ed019347ac3864002f85c116f1",
"semantic_hash": ""
},
"src/adapters/base.py": {
"mtime": 1787264412.2437606,
"seen": 1787264519.0542011,
"ast_hash": "220635d5d74e73f58259069bf5207908",
"semantic_hash": ""
},
"src/adapters/embeddings.py": {
"mtime": 1787264437.92723,
"seen": 1787264519.0542023,
"ast_hash": "7beb0ecfb7482c1fd10422da99f69abe",
"semantic_hash": ""
},
"src/adapters/llm.py": {
"mtime": 1787321869.4176295,
"seen": 1787322035.8922434,
"ast_hash": "357b505df0d92f76b1d0cd606741d1a5",
"semantic_hash": ""
},
"src/classifier.py": {
"mtime": 1787321309.8981817,
"seen": 1787321481.1505442,
"ast_hash": "a4e5dafed12aa4eaf096988b2c6a8ae0",
"semantic_hash": ""
},
"src/language.py": {
"mtime": 1787264437.9312274,
"seen": 1787264519.0542057,
"ast_hash": "cbaf52272ebb05372e34a05cd201c270",
"semantic_hash": ""
},
"src/models.py": {
"mtime": 1787264437.9302285,
"seen": 1787264519.054207,
"ast_hash": "16e604c14f7d66634dc9e2ed7959dd7c",
"semantic_hash": ""
},
"src/parser.py": {
"mtime": 1787264437.9312274,
"seen": 1787264519.054208,
"ast_hash": "d43fca3f063534e3626f86f36a97a2cc",
"semantic_hash": ""
},
"tests/__init__.py": {
"mtime": 1787195847.5186412,
"seen": 1787196463.095328,
"ast_hash": "42d67f813e6eeac2ffca24fe6206aa9b",
"semantic_hash": ""
},
"tests/test_adapters.py": {
"mtime": 1787264437.9292288,
"seen": 1787264519.0543096,
"ast_hash": "7ed9ba6f62fef404bfc28a2e16ada647",
"semantic_hash": ""
},
"tests/test_benchmark_24.py": {
"mtime": 1787264437.9302285,
"seen": 1787264519.0543122,
"ast_hash": "052a51561002be035de1a79455256b67",
"semantic_hash": ""
},
"tests/test_classifier.py": {
"mtime": 1787264437.92723,
"seen": 1787264519.0543132,
"ast_hash": "14da0c7a7d4c08762437ca777c086dea",
"semantic_hash": ""
},
"tests/test_cli.py": {
"mtime": 1787264437.9292288,
"seen": 1787264519.0543146,
"ast_hash": "b6f57994937363f8a1b0f2f66ea53d3a",
"semantic_hash": ""
},
"tests/test_language.py": {
"mtime": 1787264437.9292288,
"seen": 1787264519.0543184,
"ast_hash": "f1a09702410864434274f1b347769742",
"semantic_hash": ""
},
"tests/test_models.py": {
"mtime": 1787264437.9302285,
"seen": 1787264519.0543194,
"ast_hash": "7ec72b612bf758be8c9a3617ce18b132",
"semantic_hash": ""
},
"examples/content_northvolt_de.md": {
"mtime": 1787195963.813741,
"seen": 1787196463.1020777,
"ast_hash": "9033a8bdde7f2caba952eb0f50af8ba6",
"semantic_hash": ""
},
"examples/content_presal_pt.md": {
"mtime": 1787195950.0247998,
"seen": 1787196463.10208,
"ast_hash": "d08c26af041b833fbc9e6673aec4ee80",
"semantic_hash": ""
},
"examples/content_tangential_es.md": {
"mtime": 1787195975.5271506,
"seen": 1787196463.1020815,
"ast_hash": "55f37cf83b54926e1f5751a4ba6335a9",
"semantic_hash": ""
},
"requirements.txt": {
"mtime": 1787317345.9253972,
"seen": 1787317712.8213654,
"ast_hash": "f3f8ea2b8cc995de218d81679ec35231",
"semantic_hash": ""
},
"tests/fixtures/benchmark_24/de/contextual.md": {
"mtime": 1787196180.7794595,
"seen": 1787196463.103077,
"ast_hash": "1e78706552a34512ac2a2b772ec2ecd1",
"semantic_hash": ""
},
"tests/fixtures/benchmark_24/de/direct.md": {
"mtime": 1787196169.5728126,
"seen": 1787196463.1030784,
"ast_hash": "25f60b38e35c96356d7ecbabdd6de545",
"semantic_hash": ""
},
"tests/fixtures/benchmark_24/de/not_related.md": {
"mtime": 1787196204.0937815,
"seen": 1787196463.1030807,
"ast_hash": "6d6165d5279cda9cde833a6aa2d71164",
"semantic_hash": ""
},
"tests/fixtures/benchmark_24/de/tangential.md": {
"mtime": 1787196193.1870873,
"seen": 1787196463.1030822,
"ast_hash": "3f5ddf01ca728aa2d0116882604718cf",
"semantic_hash": ""
},
"tests/fixtures/benchmark_24/en/contextual.md": {
"mtime": 1787196066.4274058,
"seen": 1787196463.1030838,
"ast_hash": "89114901dc8579adafa4fb503afbc761",
"semantic_hash": ""
},
"tests/fixtures/benchmark_24/en/direct.md": {
"mtime": 1787196054.1496131,
"seen": 1787196463.1030855,
"ast_hash": "dd15e4f08c2757ee499da0bea578c04c",
"semantic_hash": ""
},
"tests/fixtures/benchmark_24/en/not_related.md": {
"mtime": 1787196350.3334656,
"seen": 1787196463.103087,
"ast_hash": "9e2c91a29282eac5d4c21c38bef9767e",
"semantic_hash": ""
},
"tests/fixtures/benchmark_24/en/tangential.md": {
"mtime": 1787196082.7302663,
"seen": 1787196463.1030884,
"ast_hash": "fe681417b697406ea16e0e78f2df8ce4",
"semantic_hash": ""
},
"tests/fixtures/benchmark_24/es/contextual.md": {
"mtime": 1787196129.959492,
"seen": 1787196463.1030898,
"ast_hash": "8668670e9a018ba879fb256dbe5bd2fa",
"semantic_hash": ""
},
"tests/fixtures/benchmark_24/es/direct.md": {
"mtime": 1787196118.6405888,
"seen": 1787196463.103091,
"ast_hash": "d01aa9c2cda827336558b993098fa4a1",
"semantic_hash": ""
},
"tests/fixtures/benchmark_24/es/not_related.md": {
"mtime": 1787196152.6110358,
"seen": 1787196463.1030927,
"ast_hash": "720215fd2a9ec45f745564869b20a19d",
"semantic_hash": ""
},
"tests/fixtures/benchmark_24/es/tangential.md": {
"mtime": 1787196141.436479,
"seen": 1787196463.1030943,
"ast_hash": "ef16422876c7925ea0b2631483468f12",
"semantic_hash": ""
},
"tests/fixtures/benchmark_24/fr/contextual.md": {
"mtime": 1787196288.4152596,
"seen": 1787196463.1030955,
"ast_hash": "bf01dc91dc0a8abe21eca424fb44d0c4",
"semantic_hash": ""
},
"tests/fixtures/benchmark_24/fr/direct.md": {
"mtime": 1787196277.0201283,
"seen": 1787196463.1030967,
"ast_hash": "772cc74cec96c666c7e348f600720e14",
"semantic_hash": ""
},
"tests/fixtures/benchmark_24/fr/not_related.md": {
"mtime": 1787196312.47212,
"seen": 1787196463.1030984,
"ast_hash": "da5869eda42812d47e82a4cabf77f984",
"semantic_hash": ""
},
"tests/fixtures/benchmark_24/fr/tangential.md": {
"mtime": 1787196301.2256103,
"seen": 1787196463.103103,
"ast_hash": "6926ee30a0f12424c3c4f6691daeb1fd",
"semantic_hash": ""
},
"tests/fixtures/benchmark_24/it/contextual.md": {
"mtime": 1787196234.462201,
"seen": 1787196463.1031046,
"ast_hash": "53dfa9859257d99d54b3479702abbd11",
"semantic_hash": ""
},
"tests/fixtures/benchmark_24/it/direct.md": {
"mtime": 1787196221.6644733,
"seen": 1787196463.1031058,
"ast_hash": "493cbc253516b219d6b0d745bf6c750f",
"semantic_hash": ""
},
"tests/fixtures/benchmark_24/it/not_related.md": {
"mtime": 1787196259.3629923,
"seen": 1787196463.1031072,
"ast_hash": "3fbc57bc9b9c78af4b6eb93343cc4634",
"semantic_hash": ""
},
"tests/fixtures/benchmark_24/it/tangential.md": {
"mtime": 1787196245.9183564,
"seen": 1787196463.1031086,
"ast_hash": "43f800646fc2a4060528e7ef851e8d77",
"semantic_hash": ""
},
"tests/fixtures/benchmark_24/pt/contextual.md": {
"mtime": 1787196014.2010715,
"seen": 1787196463.10311,
"ast_hash": "2e5b837c09ac05daae5007235e2a85b5",
"semantic_hash": ""
},
"tests/fixtures/benchmark_24/pt/direct.md": {
"mtime": 1787196003.236562,
"seen": 1787196463.1031117,
"ast_hash": "f7e5ca68966f5e2b805fde6d46b90f7e",
"semantic_hash": ""
},
"tests/fixtures/benchmark_24/pt/not_related.md": {
"mtime": 1787196036.7240841,
"seen": 1787196463.103113,
"ast_hash": "980a58ca0db93f8dbb6c43e1c9108baf",
"semantic_hash": ""
},
"tests/fixtures/benchmark_24/pt/tangential.md": {
"mtime": 1787196024.9788618,
"seen": 1787196463.1031141,
"ast_hash": "e05ab20a5190cfb5b9d41da64d5273cf",
"semantic_hash": ""
},
"tests/test_adversarial.py": {
"mtime": 1787264437.9242287,
"seen": 1787264519.0543108,
"ast_hash": "d353633ce31a624a8dcda790efec4d5e",
"semantic_hash": ""
},
"scripts/extract_google_news.py": {
"mtime": 1787264437.92723,
"seen": 1787264519.0540311,
"ast_hash": "97d8a193901d5e2edf6359fad256b7ef",
"semantic_hash": ""
},
"tests/test_extract_google_news.py": {
"mtime": 1787264437.9312274,
"seen": 1787264519.054317,
"ast_hash": "7561ad8fedcf1bc6cc8ed1e86a345532",
"semantic_hash": ""
},
"docs/googlenews_extractor_guia_completo.md": {
"mtime": 1787264437.9282286,
"seen": 1787264519.0578,
"ast_hash": "d7c3c64e2009dded496a08c7c91f63b9",
"semantic_hash": ""
},
"specs/002-google-news-extractor/checklists/readiness.md": {
"mtime": 1787234305.932687,
"seen": 1787234772.8123577,
"ast_hash": "a1200a6959d41e256a73403dd5c4e1f6",
"semantic_hash": ""
},
"specs/002-google-news-extractor/checklists/requirements.md": {
"mtime": 1787232386.6763985,
"seen": 1787234772.812359,
"ast_hash": "39cd86af76c9f74baa2984418870b677",
"semantic_hash": ""
},
"specs/002-google-news-extractor/contracts/cli_contract.md": {
"mtime": 1787236744.3139226,
"seen": 1787236817.4979818,
"ast_hash": "76a61ad31df989244edc5e739081bf7b",
"semantic_hash": ""
},
"specs/002-google-news-extractor/data-model.md": {
"mtime": 1787234040.8134317,
"seen": 1787234772.8123617,
"ast_hash": "e8380c98d2f6418f60ac8a511ecdb637",
"semantic_hash": ""
},
"specs/002-google-news-extractor/plan.md": {
"mtime": 1787236725.9825225,
"seen": 1787236817.4983613,
"ast_hash": "a787c974414ae6c173d4c39e76113c31",
"semantic_hash": ""
},
"specs/002-google-news-extractor/quickstart.md": {
"mtime": 1787236764.0206513,
"seen": 1787236817.4983652,
"ast_hash": "ada22d34a0fa8341f88136cf42f055a9",
"semantic_hash": ""
},
"specs/002-google-news-extractor/research.md": {
"mtime": 1787236785.0466182,
"seen": 1787236817.498368,
"ast_hash": "60837e6c7463c4413911ceca0087374c",
"semantic_hash": ""
},
"specs/002-google-news-extractor/spec.md": {
"mtime": 1787236709.0783155,
"seen": 1787236817.4983711,
"ast_hash": "dfd963f7ca351c93e09f74af9b11d19e",
"semantic_hash": ""
},
"specs/002-google-news-extractor/tasks.md": {
"mtime": 1787236802.4808047,
"seen": 1787236817.4983742,
"ast_hash": "427f24026deaa7c9aca95d99e7587ae6",
"semantic_hash": ""
},
"scripts/__init__.py": {
"mtime": 1787234973.5782337,
"seen": 1787235020.6850297,
"ast_hash": "627a6c953b250083ebe76ee74d605bb5",
"semantic_hash": ""
},
"README.md": {
"mtime": 1787321467.0542295,
"seen": 1787321481.1561577,
"ast_hash": "0cde8e800125cbba61a1d7de9d9d2c9e",
"semantic_hash": ""
},
"scripts/extract_article_contents.py": {
"mtime": 1787264465.3415215,
"seen": 1787264519.0540295,
"ast_hash": "2bea6b2a048526cb49d95fb88a001600",
"semantic_hash": ""
},
"tests/test_extract_article_contents.py": {
"mtime": 1787264437.9292288,
"seen": 1787264519.0543158,
"ast_hash": "56aee72480f05c714865791a935d539c",
"semantic_hash": ""
},
"docs/prd_extrator_artigos_nlp.md": {
"mtime": 1787237774.0092583,
"seen": 1787240516.5991437,
"ast_hash": "c90f25764acefb91f1160feaf497fb33",
"semantic_hash": ""
},
"specs/003-article-content-extractor/checklists/extraction.md": {
"mtime": 1787239873.797626,
"seen": 1787240516.6008725,
"ast_hash": "3c78fb563727a7f6f23240dcaad3c010",
"semantic_hash": ""
},
"specs/003-article-content-extractor/checklists/requirements.md": {
"mtime": 1787237950.8140705,
"seen": 1787240516.600874,
"ast_hash": "16b2bf7a7910722a7aa6c57cdcc31f1c",
"semantic_hash": ""
},
"specs/003-article-content-extractor/contracts/cli-contract.md": {
"mtime": 1787239569.0560138,
"seen": 1787240516.6008754,
"ast_hash": "9326b1b3c124ad41636c3fae5c9de8b5",
"semantic_hash": ""
},
"specs/003-article-content-extractor/contracts/json-schema.md": {
"mtime": 1787239578.9314032,
"seen": 1787240516.6008763,
"ast_hash": "93f1da07b16c61de15dc93071434d145",
"semantic_hash": ""
},
"specs/003-article-content-extractor/data-model.md": {
"mtime": 1787239558.2362826,
"seen": 1787240516.6008773,
"ast_hash": "03ed32de948199c9a498c486e7bdafbd",
"semantic_hash": ""
},
"specs/003-article-content-extractor/plan.md": {
"mtime": 1787264323.707266,
"seen": 1787264519.0605621,
"ast_hash": "b2232491829a303a49e749c75f3adca2",
"semantic_hash": ""
},
"specs/003-article-content-extractor/quickstart.md": {
"mtime": 1787239589.9896657,
"seen": 1787240516.6008794,
"ast_hash": "7cac454150d33dad67f683367fca749f",
"semantic_hash": ""
},
"specs/003-article-content-extractor/research.md": {
"mtime": 1787239545.8330145,
"seen": 1787240516.6008804,
"ast_hash": "beef81df32e2dc16798ece67726daa95",
"semantic_hash": ""
},
"specs/003-article-content-extractor/spec.md": {
"mtime": 1787264319.2883234,
"seen": 1787264519.060745,
"ast_hash": "187b1307e28a02f0ac44895b5f9cca55",
"semantic_hash": ""
},
"specs/003-article-content-extractor/tasks.md": {
"mtime": 1787240464.6520643,
"seen": 1787240516.6008823,
"ast_hash": "d95c4763728496703cd89590288a2bed",
"semantic_hash": ""
},
"scripts/select_article_extractor.py": {
"mtime": 1787274502.4801269,
"seen": 1787310813.8455508,
"ast_hash": "4037eda779d5fc81527d0698a8fc07d6",
"semantic_hash": ""
},
"tests/test_select_article_extractor.py": {
"mtime": 1787274502.4801269,
"seen": 1787310813.8470025,
"ast_hash": "2cd19eb0c5204a8c22b7727e259db885",
"semantic_hash": ""
},
"docs/prd_deterministic_content_selection.md": {
"mtime": 1787264674.2038286,
"seen": 1787272611.6604555,
"ast_hash": "2c421402df8b6469ce60eaa984dce666",
"semantic_hash": ""
},
"specs/004-deterministic-content-selection/checklists/deterministic-selection.md": {
"mtime": 1787272346.8692143,
"seen": 1787272611.6713927,
"ast_hash": "6d9bcda99dbd284ffcbce372102fd868",
"semantic_hash": ""
},
"specs/004-deterministic-content-selection/checklists/requirements.md": {
"mtime": 1787264728.7202728,
"seen": 1787272611.6713977,
"ast_hash": "074efe6ba551e93f7d005c5462805693",
"semantic_hash": ""
},
"specs/004-deterministic-content-selection/contracts/cli-contract.md": {
"mtime": 1787274329.2175446,
"seen": 1787274402.0944166,
"ast_hash": "2cd795eaed6198dc343424bd20673728",
"semantic_hash": ""
},
"specs/004-deterministic-content-selection/contracts/json-schema.md": {
"mtime": 1787271837.7328393,
"seen": 1787272611.6714034,
"ast_hash": "155f0bccb7bcffabcedb6ef86b8003c5",
"semantic_hash": ""
},
"specs/004-deterministic-content-selection/data-model.md": {
"mtime": 1787271825.5970395,
"seen": 1787272611.671406,
"ast_hash": "23ec4a144f4aeeed9fee259b6165bea3",
"semantic_hash": ""
},
"specs/004-deterministic-content-selection/plan.md": {
"mtime": 1787271850.1744206,
"seen": 1787272611.671409,
"ast_hash": "3bebc2a73a9632d42ccca646af592133",
"semantic_hash": ""
},
"specs/004-deterministic-content-selection/quickstart.md": {
"mtime": 1787274502.478128,
"seen": 1787310813.854652,
"ast_hash": "60453533ee75a4f0a894dca634e0c96c",
"semantic_hash": ""
},
"specs/004-deterministic-content-selection/research.md": {
"mtime": 1787274316.8544433,
"seen": 1787274402.0946283,
"ast_hash": "73ac1637d31472e25b0a1709cd137e5c",
"semantic_hash": ""
},
"specs/004-deterministic-content-selection/spec.md": {
"mtime": 1787274311.1564865,
"seen": 1787274402.0946293,
"ast_hash": "b2c994a14871c2e6e1cf4a11db7dbeb5",
"semantic_hash": ""
},
"specs/004-deterministic-content-selection/tasks.md": {
"mtime": 1787272595.8186145,
"seen": 1787272611.6714194,
"ast_hash": "3a2617478f44c6400ed57088d2039f93",
"semantic_hash": ""
},
"scripts/convert_article_to_markdown.py": {
"mtime": 1787320071.2809863,
"seen": 1787320239.0233297,
"ast_hash": "7939a71acd9b264f9d00eab5c652cd36",
"semantic_hash": ""
},
"tests/test_convert_article_to_markdown.py": {
"mtime": 1787320111.811304,
"seen": 1787320239.0292969,
"ast_hash": "5223f35131b8e952e05c44710f3988b2",
"semantic_hash": ""
},
"docs/prd_convert_json_markdown.md": {
"mtime": 1787315400.452784,
"seen": 1787317712.8208659,
"ast_hash": "1fc73d15d4f4d5c1e74f299040b040f2",
"semantic_hash": ""
},
"specs/005-convert-json-markdown/checklists/markdown-conversion.md": {
"mtime": 1787315812.9198053,
"seen": 1787317712.8255877,
"ast_hash": "4cc2161c66aa34dc8540c48322b6eaaa",
"semantic_hash": ""
},
"specs/005-convert-json-markdown/checklists/requirements.md": {
"mtime": 1787315513.450848,
"seen": 1787317712.8255897,
"ast_hash": "a14c648defdfb7c2ad77601249d44a66",
"semantic_hash": ""
},
"specs/005-convert-json-markdown/contracts/cli-contract.md": {
"mtime": 1787315656.382816,
"seen": 1787317712.8255913,
"ast_hash": "bba24a6fcd59b265d11242b4b09d2537",
"semantic_hash": ""
},
"specs/005-convert-json-markdown/contracts/markdown-schema.md": {
"mtime": 1787315662.3060155,
"seen": 1787317712.8255925,
"ast_hash": "4dd6023205dcc49447bf3c78f0419c52",
"semantic_hash": ""
},
"specs/005-convert-json-markdown/data-model.md": {
"mtime": 1787315648.195198,
"seen": 1787317712.8255942,
"ast_hash": "31de71b9fa43fb1b257f76ae2de33044",
"semantic_hash": ""
},
"specs/005-convert-json-markdown/plan.md": {
"mtime": 1787315676.826685,
"seen": 1787317712.8255954,
"ast_hash": "44e3074130dadf889bf3830e6139b91a",
"semantic_hash": ""
},
"specs/005-convert-json-markdown/quickstart.md": {
"mtime": 1787315668.269243,
"seen": 1787317712.8255966,
"ast_hash": "6086387edc574ed31202ff05eaa3fb54",
"semantic_hash": ""
},
"specs/005-convert-json-markdown/research.md": {
"mtime": 1787315639.7204154,
"seen": 1787317712.825598,
"ast_hash": "3ffedbbf7e61245b86119f41a5f8af2d",
"semantic_hash": ""
},
"specs/005-convert-json-markdown/spec.md": {
"mtime": 1787315504.210701,
"seen": 1787317712.825599,
"ast_hash": "9f78ac1c71853dcf4e364c66973dfcbc",
"semantic_hash": ""
},
"specs/005-convert-json-markdown/tasks.md": {
"mtime": 1787317701.507181,
"seen": 1787317712.8256,
"ast_hash": "1287da802887af5e0dd3a6a2685f5fc0",
"semantic_hash": ""
},
"tests/fixtures/markdown_conversion/valid_newspaper4k.md": {
"mtime": 1787317400.9745035,
"seen": 1787317712.8280056,
"ast_hash": "3bfb28bbde97c16635de39bbbe6fbd8b",
"semantic_hash": ""
},
"tests/fixtures/markdown_conversion/valid_readability.md": {
"mtime": 1787317419.4019263,
"seen": 1787317712.8280075,
"ast_hash": "49fdc6bedd7585eda33e2c502337e01a",
"semantic_hash": ""
},
"tests/fixtures/markdown_conversion/valid_trafilatura.md": {
"mtime": 1787317383.5058665,
"seen": 1787317712.8280091,
"ast_hash": "c63c1c39e34e08a239aa8bea3c264756",
"semantic_hash": ""
},
"tests/test_llm_fallback.py": {
"mtime": 1787320797.249565,
"seen": 1787320818.2967606,
"ast_hash": "e5d98de814ceeecd8bd601a7c206e92d",
"semantic_hash": ""
},
"tests/test_e2e_text_analysis_pipeline.py": {
"mtime": 1787321086.751707,
"seen": 1787321205.5156026,
"ast_hash": "3a2d47f2ffcf8371ffdf90bb797b5346",
"semantic_hash": ""
},
"tests/test_classify_exhaustive_suite.py": {
"mtime": 1787321371.2658408,
"seen": 1787321481.15137,
"ast_hash": "08e3c680d8669b2849a19d73b27b869a",
"semantic_hash": ""
}
}
+320 -70
View File
@@ -1,16 +1,16 @@
# Graph Report - TextNLPClassifierApp (2026-08-20) # Graph Report - TextNLPClassifierApp (2026-08-21)
## Corpus Check ## Corpus Check
- 161 files · ~78,549 words - 204 files · ~115,644 words
- Verdict: corpus is large enough that graph structure adds value. - Verdict: corpus is large enough that graph structure adds value.
## Summary ## Summary
- 1000 nodes · 1220 edges · 115 communities (77 shown, 38 thin omitted) - 1658 nodes · 2226 edges · 174 communities (126 shown, 48 thin omitted)
- Extraction: 97% EXTRACTED · 3% INFERRED · 0% AMBIGUOUS · INFERRED: 39 edges (avg confidence: 0.95) - Extraction: 94% EXTRACTED · 6% INFERRED · 0% AMBIGUOUS · INFERRED: 127 edges (avg confidence: 0.95)
- Token cost: 0 input · 0 output - Token cost: 0 input · 0 output
## Graph Freshness ## Graph Freshness
- Built from commit: `6e3d5761` - Built from commit: `040edb61`
- Run `git rev-parse HEAD` and compare to check if the graph is stale. - Run `git rev-parse HEAD` and compare to check if the graph is stale.
- Run `graphify update .` after code changes (no API cost). - Run `graphify update .` after code changes (no API cost).
@@ -56,10 +56,10 @@
- 1. Input Schemas - 1. Input Schemas
- 2. Basic CLI Usage Examples - 2. Basic CLI Usage Examples
- 2. Standard Streams & Exit Codes - 2. Standard Streams & Exit Codes
- classifier.py - ClassificationResult
- test_adversarial.py
- InherenceClassifier - InherenceClassifier
- detect_language - test_convert_article_to_markdown.py
- ClassificationError
- content_northvolt_de.md - content_northvolt_de.md
- content_presal_pt.md - content_presal_pt.md
- content_tangential_es.md - content_tangential_es.md
@@ -109,13 +109,13 @@
- CLI Contract: Google News Headlines Extractor - CLI Contract: Google News Headlines Extractor
- 🧠 TextNLPClassifierApp - 🧠 TextNLPClassifierApp
- Extraction Pipeline Checklist: Article Content Multi-Engine Extractor - Extraction Pipeline Checklist: Article Content Multi-Engine Extractor
- sample_rss_xml - parametrize
- Path
- main - main
- ECPSnapshot
- Feature Specification: Multilingual NLP Entity Inherence Classifier (POC) - Feature Specification: Multilingual NLP Entity Inherence Classifier (POC)
- 4. Requisitos Funcionais (FR) - 4. Requisitos Funcionais (FR)
- Tasks: Article Content Multi-Engine Extractor - Tasks: Article Content Multi-Engine Extractor
- models.py - Tasks: Convert Article JSON to Markdown
- Implementation Plan: Article Content Multi-Engine Extractor - Implementation Plan: Article Content Multi-Engine Extractor
- 2. Cenários de Validação - 2. Cenários de Validação
- 1. Technical Decisions & Tradeoffs - 1. Technical Decisions & Tradeoffs
@@ -124,35 +124,93 @@
- Specification Quality Checklist: Multilingual NLP Entity Inherence Classifier - Specification Quality Checklist: Multilingual NLP Entity Inherence Classifier
- CLI Contract: Article Content Multi-Engine Extractor - CLI Contract: Article Content Multi-Engine Extractor
- JSON Schema Contract: Article Content Multi-Engine Extractor - JSON Schema Contract: Article Content Multi-Engine Extractor
- PRD — Seleção determinística da biblioteca de extração de conteúdo
- select_article_extractor.py
- 1. Text Normalization Pipeline
- Tasks: Deterministic Article Content Selection
- select_article_extractor
- process_batch
- detect_language
- test_select_article_extractor.py
- Feature Specification: Deterministic Content Selection
- 2. Entity Descriptions & Fields
- Implementation Plan: Deterministic Article Content Selection
- Deterministic Content Selection Checklist: End-to-End Requirements Quality
- Quickstart: Deterministic Article Content Selection
- Specification Quality Checklist: Deterministic Content Selection
- CLI Interface Contract: Deterministic Article Content Selection
- convert_article_to_markdown.py
- 8. Regras funcionais
- 12. Critérios de aceite
- PRD — Conversão de artigo JSON para Markdown
- resolve_article_body
- Implementation Plan: Convert Article JSON to Markdown
- 2. Technical Decisions & Research Findings
- Feature Specification: Convert Article JSON to Markdown
- Markdown Conversion Checklist: End-to-End Requirements Quality
- convert_article
- 005-convert-json-markdown/plan.md
- Quickstart: Convert Article JSON to Markdown
- 1. Domain Entities & Schemas
- 11. Requisitos não funcionais
- parse_arguments
- Specification Quality Checklist: Convert Article JSON to Markdown
- CLI Contract: `convert_article_to_markdown.py`
- 9. Interface CLI
- sample_rss_xml
- 13. Estratégia de testes
- 6. Contrato de entrada
- ECPSnapshot
- convert_html_to_markdown
- JSON Schema Contract: Deterministic Article Content Selection
- 5. Escopo
- Los puntajes de River vs. Independiente Santa Fe, por la Copa Sudamericana - TyC Sports
- valid_newspaper4k.md
- valid_readability.md
- test_normalize_list_author_url_filtering
- test_normalize_date_invalid_and_placeholders
- test_normalize_list_deduplication_preserves_case_and_order
- test_normalize_date_iso_8601_variants
- test_metadata_priority_original_url_all_fallbacks
- test_normalize_scalar_non_string_types
- test_e2e_text_analysis_pipeline.py
- remove_duplicate_initial_h1
- test_normalize_scalar_whitespace_collapsing
- LLMFallbackAdapter
- test_funnel_cli_subprocess_end_to_end
- test_models.py
- extract_evidence_snippets
- .classify
- 🧪 Documentação da Suíte de Testes Automatizados
## God Nodes (most connected - your core abstractions) ## God Nodes (most connected - your core abstractions)
1. `ECPSnapshot` - 31 edges 1. `ECPSnapshot` - 78 edges
2. `InherenceClassifier` - 25 edges 2. `InherenceClassifier` - 62 edges
3. `DecisionCategory` - 17 edges 3. `DecisionCategory` - 62 edges
4. `ClassificationResult` - 17 edges 4. `LLMFallbackAdapter` - 48 edges
5. `process_batch()` - 15 edges 5. `ClassificationResult` - 29 edges
6. `LocalEmbeddingsAdapter` - 14 edges 6. `select_article_extractor()` - 23 edges
7. `LLMFallbackAdapter` - 14 edges 7. `ExtractorName` - 21 edges
8. `detect_language()` - 14 edges 8. `main()` - 20 edges
9. `main()` - 13 edges 9. `PRD — Conversão de artigo JSON para Markdown` - 16 edges
10. `ArticleCrawler` - 13 edges 10. `process_batch()` - 15 edges
## Surprising Connections (you probably didn't know these) ## Surprising Connections (you probably didn't know these)
- `main()` --uses--> `ECPSnapshot` [INFERRED] - `main()` --uses--> `ECPSnapshot` [INFERRED]
classify.py → src/models.py classify.py → src/models.py
- `test_extract_google_news_orchestration_mocked()` --uses--> `ExtractionResult` [INFERRED] - `test_extract_google_news_orchestration_mocked()` --uses--> `ExtractionResult` [INFERRED]
tests/test_extract_google_news.py → scripts/extract_google_news.py tests/test_extract_google_news.py → scripts/extract_google_news.py
- `test_embeddings_adapter_interface()` --calls--> `LocalEmbeddingsAdapter` [EXTRACTED]
tests/test_adapters.py → src/adapters/embeddings.py
- `classifier()` --uses--> `InherenceClassifier` [INFERRED] - `classifier()` --uses--> `InherenceClassifier` [INFERRED]
tests/test_benchmark_24.py → src/classifier.py tests/test_benchmark_24.py → src/classifier.py
- `test_classification_result_serialization()` --uses--> `DecisionCategory` [INFERRED] - `test_funnel_multilingual_language_detection()` --uses--> `InherenceClassifier` [INFERRED]
tests/test_models.py → src/models.py tests/test_e2e_text_analysis_pipeline.py → src/classifier.py
- `test_classification_error_serialization()` --uses--> `ErrorCode` [INFERRED]
tests/test_models.py → src/models.py
## Import Cycles ## Import Cycles
- None detected. - None detected.
## Communities (115 total, 38 thin omitted) ## Communities (174 total, 48 thin omitted)
### Community 0 - "Task Planning" ### Community 0 - "Task Planning"
Cohesion: 0.07 Cohesion: 0.07
@@ -294,21 +352,21 @@ Nodes (7): 1. Prerequisites & Installation, 2.1 Direct Inherence (Portuguese), 2
Cohesion: 0.29 Cohesion: 0.29
Nodes (6): 1.1 Arguments & Options, 1. Command Line Interface, 2.1 Exit Codes, 2.2 Standard Output (`stdout`) / Standard Error (`stderr`), 2. Standard Streams & Exit Codes, CLI Contract & Interface Specification (POC) Nodes (6): 1.1 Arguments & Options, 1. Command Line Interface, 2.1 Exit Codes, 2.2 Standard Output (`stdout`) / Standard Error (`stderr`), 2. Standard Streams & Exit Codes, CLI Contract & Interface Specification (POC)
### Community 45 - "classifier.py" ### Community 45 - "ClassificationResult"
Cohesion: 0.14
Nodes (9): LocalEmbeddingsAdapter, Optional adapter for local multilingual semantic vector embeddings., LLMFallbackAdapter, Optional adapter for LLM fallback boundary disambiguation., Core deterministic classification engine (Tier 1 core)., Unit tests for optional adapter interfaces (Tier 2 / Tier 3)., test_classifier_with_adapter_flags(), test_embeddings_adapter_interface() (+1 more)
### Community 46 - "InherenceClassifier"
Cohesion: 0.12
Nodes (27): InherenceClassifier, Tier 1 Deterministic NLP Entity Inherence Classifier., DecisionCategory, RelatedEntity, Adversarial and robustness test suite for Multilingual NLP Entity Inherence…, Run CLI via subprocess without --output and verify stdout is pure parseable…, Run CLI via subprocess with empty content and verify error code and exit code., Content about city/state governance of São Paulo against ECP for São Paulo FC. (+19 more)
### Community 47 - "detect_language"
Cohesion: 0.09 Cohesion: 0.09
Nodes (30): count_phrase_occurrences(), match_phrase_in_text(), Check if a normalized phrase appears in normalized text with word boundary…, Count occurrences of a phrase in text., Classify inherence of content against an ECP snapshot., detect_language(), extract_words(), normalize_text() (+22 more) Nodes (22): ABC, BaseNLPAdapter, Base abstract adapter interface for optional Tier 2 / Tier 3 NLP enhancers., Abstract interface for pluggable NLP classification adapters., Return True if the underlying provider or model is installed and configured., Compute semantic similarity score between text and a set of candidate terms., Optionally refine an ambiguous classification result., LocalEmbeddingsAdapter (+14 more)
### Community 48 - "ClassificationError" ### Community 46 - "test_adversarial.py"
Cohesion: 0.29 Cohesion: 0.12
Nodes (3): ClassificationError, Any, test_classification_error_serialization() Nodes (16): RelatedEntity, Adversarial and robustness test suite for Multilingual NLP Entity Inherence…, Run CLI via subprocess without --output and verify stdout is pure parseable…, Run CLI via subprocess with empty content and verify error code and exit code., Run CLI via subprocess with missing target_name and verify error payload., Run CLI via subprocess with corrupted JSON and verify error payload., Content about apple fruit/culinary recipe against Apple Inc. tech entity., High-weight related entity mentioned in passing without required domain anchors. (+8 more)
### Community 47 - "InherenceClassifier"
Cohesion: 0.06
Nodes (63): InherenceClassifier, Tier 1 Deterministic NLP Entity Inherence Classifier with optional Tier 2 /…, DecisionCategory, Content about city/state governance of São Paulo against ECP for São Paulo FC., test_adversarial_sao_paulo_city_vs_fc(), Unit tests for deterministic classification decision logic., test_contextual_inherent(), test_direct_inherent() (+55 more)
### Community 48 - "test_convert_article_to_markdown.py"
Cohesion: 0.08
Nodes (25): Suíte de Testes Automatizados para Conversão de Artigo JSON para Markdown.…, Testa divisão por ponto e vírgula na string e vírgulas em elementos de lista…, Valida parsing de datas no formato RFC 2822 (usado em feeds RSS e cabeçalhos…, Valida a cadeia de fallback completa para o campo TÍTULO (6 níveis)., Garante que subtítulo idêntico ao título seja automaticamente omitido (None)., Valida decodificação de entidades HTML nomeadas e numéricas., Garante correspondência exata byte a byte para Trafilatura, Newspaper4k e…, Garante que múltiplas execuções no mesmo arquivo produzam hashes SHA-256… (+17 more)
### Community 80 - "test_extract_article_contents.py" ### Community 80 - "test_extract_article_contents.py"
Cohesion: 0.06 Cohesion: 0.06
@@ -371,24 +429,24 @@ Cohesion: 0.33
Nodes (6): 1. Comando e Argumentos, 2. Códigos de Saída (Exit Codes), 3. Protocolo de Streams (Stdout / Stderr), Argumentos de Linha de Comando, CLI Contract: Google News Headlines Extractor, Sintaxe Nodes (6): 1. Comando e Argumentos, 2. Códigos de Saída (Exit Codes), 3. Protocolo de Streams (Stdout / Stderr), Argumentos de Linha de Comando, CLI Contract: Google News Headlines Extractor, Sintaxe
### Community 97 - "🧠 TextNLPClassifierApp" ### Community 97 - "🧠 TextNLPClassifierApp"
Cohesion: 0.06 Cohesion: 0.04
Nodes (33): 1. 🧠 Classificador de Conteúdo e Inerência (NLP / LLM / ECP), 1. Clonar o Repositório e Criar Ambiente Virtual, 1. Extração Completa Automática, 1. River Plate (Argentina / Espanhol / 2 Páginas / Salvar em Arquivo), 2. Amostragem Rápida (Limit 2 Notícias), 2. Cruzeiro (Brasil / Português / Formatado no Terminal), 2. 📰 Extrator de Manchetes do Google News, 2. Instalar Dependências (+25 more) Nodes (45): 1. 🧠 Classificador de Conteúdo e Inerência (NLP / LLM / ECP), 1. Clonar o Repositório e Criar Ambiente Virtual, 1. Conversão Padrão, 1. Execução Padrão Automática, 2. Conversão com Caminho de Destino Personalizado, 2. Execução com Modo Verboso, 2. 📰 Extrator de Manchetes do Google News, 2. Instalar Dependências (+37 more)
### Community 98 - "Extraction Pipeline Checklist: Article Content Multi-Engine Extractor" ### Community 98 - "Extraction Pipeline Checklist: Article Content Multi-Engine Extractor"
Cohesion: 0.05 Cohesion: 0.05
Nodes (34): 1. Requirement Completeness, 2. Requirement Clarity & Non-Ambiguity, 3. Requirement Consistency & Data Contracts, 4. Scenario & Edge Case Coverage, 5. Non-Functional & Operational Readiness, Extraction Pipeline Checklist: Article Content Multi-Engine Extractor, Notes, Content Quality (+26 more) Nodes (34): 1. Requirement Completeness, 2. Requirement Clarity & Non-Ambiguity, 3. Requirement Consistency & Data Contracts, 4. Scenario & Edge Case Coverage, 5. Non-Functional & Operational Readiness, Extraction Pipeline Checklist: Article Content Multi-Engine Extractor, Notes, Content Quality (+26 more)
### Community 99 - "sample_rss_xml" ### Community 99 - "parametrize"
Cohesion: 0.67
Nodes (3): fixture, Fixture que fornece o conteúdo do XML de exemplo para testes offline., sample_rss_xml()
### Community 100 - "main"
Cohesion: 0.23
Nodes (13): emit_error(), main(), parse_args(), Namespace, Enum, ErrorCode, str, CLI execution tests covering flags, arguments, stdout, and error handling. (+5 more)
### Community 101 - "ECPSnapshot"
Cohesion: 0.22 Cohesion: 0.22
Nodes (11): parametrize, ECPSnapshot, classifier(), fixture, Controlled 24-case benchmark suite for Multilingual NLP Entity Inherence…, test_benchmark_case(), Unit tests for ECP models, schema validation, and structured error handling., test_classification_result_serialization() (+3 more) Nodes (9): parametrize, Garante aceitação de URLs absolutas com esquema HTTP e HTTPS válidos., Garante rejeição de esquemas não permitidos, URLs relativas e strings vazias., Garante que a ausência de corpo no extrator selecionado NUNCA faça fallback…, Garante que todos os placeholders documentados no PRD sejam descartados…, test_normalize_scalar_placeholders_discarded(), test_resolve_article_body_strict_isolation_all_extractors(), test_validate_url_invalid_schemes() (+1 more)
### Community 100 - "Path"
Cohesion: 0.15
Nodes (13): Path, Cenário 6.1: Caminho de ECP inexistente -> Exit Code 1, error_code:…, Cenário 6.2: Arquivo ECP com sintaxe JSON corrompida., Cenário 6.3: Valida erro para falta de cada um dos campos obrigatórios do ECP., Cenário 6.4: Caminho de arquivo Markdown inexistente., Cenário 6.5: Arquivo Markdown vazio ou contendo apenas espaços em branco., Cenário 6.6: A flag -o / --output cria diretórios aninhados automaticamente., test_cli_error_content_file_does_not_exist() (+5 more)
### Community 101 - "main"
Cohesion: 0.25
Nodes (12): emit_error(), main(), parse_args(), Namespace, ErrorCode, str, CLI execution tests covering flags, arguments, stdout, and error handling., test_cli_empty_content_file() (+4 more)
### Community 102 - "Feature Specification: Multilingual NLP Entity Inherence Classifier (POC)" ### Community 102 - "Feature Specification: Multilingual NLP Entity Inherence Classifier (POC)"
Cohesion: 0.14 Cohesion: 0.14
@@ -402,9 +460,9 @@ Nodes (20): 1.1 Objetivo do Produto, 1. Visão Geral e Contexto, 2. Personas e C
Cohesion: 0.11 Cohesion: 0.11
Nodes (18): Dependencies & Execution Order, Entrega Incremental, Implementation Strategy, Implementação da User Story 1, Implementação da User Story 2, Implementação da User Story 3, MVP First (User Story 1 Only), Oportunidades de Execução Paralela (+10 more) Nodes (18): Dependencies & Execution Order, Entrega Incremental, Implementation Strategy, Implementação da User Story 1, Implementação da User Story 2, Implementação da User Story 3, MVP First (User Story 1 Only), Oportunidades de Execução Paralela (+10 more)
### Community 105 - "models.py" ### Community 105 - "Tasks: Convert Article JSON to Markdown"
Cohesion: 0.16 Cohesion: 0.11
Nodes (12): ABC, BaseNLPAdapter, Base abstract adapter interface for optional Tier 2 / Tier 3 NLP enhancers., Abstract interface for pluggable NLP classification adapters., Return True if the underlying provider or model is installed and configured., Compute semantic similarity score between text and a set of candidate terms., Optionally refine an ambiguous classification result., Optional local vector embeddings adapter (Tier 2). Disabled by default.… (+4 more) Nodes (19): Dependencies & Execution Order, Implementation for User Story 1, Implementation for User Story 2, Implementation for User Story 3, Implementation Strategy, Incremental Delivery, MVP First (User Story 1 Only), Parallel Opportunities (+11 more)
### Community 106 - "Implementation Plan: Article Content Multi-Engine Extractor" ### Community 106 - "Implementation Plan: Article Content Multi-Engine Extractor"
Cohesion: 0.17 Cohesion: 0.17
@@ -438,25 +496,217 @@ Nodes (5): 1. Comando de Execução, 2. Argumentos e Flags, 3. Códigos de Saíd
Cohesion: 0.50 Cohesion: 0.50
Nodes (3): 1. Schema de Entrada (Input JSON), 2. Schema de Saída (Output JSON), JSON Schema Contract: Article Content Multi-Engine Extractor Nodes (3): 1. Schema de Entrada (Input JSON), 2. Schema de Saída (Output JSON), JSON Schema Contract: Article Content Multi-Engine Extractor
### Community 115 - "PRD — Seleção determinística da biblioteca de extração de conteúdo"
Cohesion: 0.07
Nodes (29): 10. Requisitos não funcionais, 11. Critérios de aceite, 12. Casos obrigatórios de teste, 13. Definition of Done, 1. Contexto, 2. Objetivo, 3.1 Incluído, 3.2 Fora do escopo (+21 more)
### Community 116 - "select_article_extractor.py"
Cohesion: 0.14
Nodes (19): ArticleSelectionResult, BatchProcessingResult, break_priority_tie(), calculate_consensus_metrics(), ExtractorCandidate, form_active_set(), main(), parse_args() (+11 more)
### Community 117 - "1. Text Normalization Pipeline"
Cohesion: 0.11
Nodes (18): 1. Text Normalization Pipeline, 2. 5-Token Shingles & Consensus Metrics, 3. Regras de Decisão, Empate Técnico e Desempate Hierárquico, 4. Estratégia de I/O Não Destrutiva e Escrita Atômica, Alternatives Considered, Context, Context, Context (+10 more)
### Community 118 - "Tasks: Deterministic Article Content Selection"
Cohesion: 0.11
Nodes (18): Dependencies & Execution Order, Implementation for User Story 1, Implementation for User Story 2, Implementation for User Story 3, Implementation Strategy, Incremental Delivery (Phases 4, 5 & 6), MVP First (Phases 1, 2 & 3), Parallel Opportunities (+10 more)
### Community 119 - "select_article_extractor"
Cohesion: 0.08
Nodes (35): CandidateStatus, extract_candidate_data(), ExtractorName, Any, Enum, str, Extrai campo de texto, erro e calcula tokens/shingles para um motor., Nomes canônicos e catálogo fechado dos motores de extração. (+27 more)
### Community 120 - "process_batch"
Cohesion: 0.11
Nodes (24): atomic_save_json(), process_batch(), Path, Salva dados em JSON de forma atômica utilizando arquivo temporário e rename., Lê o JSON de entrada, valida a estrutura, processa todos os artigos e grava o…, Path, CT-012: A entrada já contém selected_extractor -> Recalcular e substituir…, CT-013: articles está vazio -> Gerar saída válida com articles vazio. (+16 more)
### Community 121 - "detect_language"
Cohesion: 0.22
Nodes (13): detect_language(), extract_words(), Lightweight multilingual language detection and text normalization., Tokenize text into lowercase alphanumeric words., Detect the ISO-639-1 language code of text among supported languages (pt, en,…, Unit tests for language detection and text normalization., test_detect_english(), test_detect_french() (+5 more)
### Community 122 - "test_select_article_extractor.py"
Cohesion: 0.18
Nodes (16): generate_shingles(), normalize_text(), Executa a normalização determinística para comparação: 1. Decodificar entidades…, Gera conjunto de shingles ordenados de tamanho window_size (padrão 5). - Se…, Suíte de Testes Automatizados para o Seletor Determinístico de Extrator. Cobre…, Garante que marcação de imagem Markdown ![alt](url) seja descartada e link…, E2E: Executa scripts/select_article_extractor.py como subprocesso real na linha…, test_e2e_cli_subprocess_real_execution() (+8 more)
### Community 123 - "Feature Specification: Deterministic Content Selection"
Cohesion: 0.17
Nodes (12): Assumptions, Edge Cases, Feature Specification: Deterministic Content Selection, Functional Requirements, Key Entities *(include if feature involves data)*, Measurable Outcomes, Requirements *(mandatory)*, Success Criteria *(mandatory)* (+4 more)
### Community 124 - "2. Entity Descriptions & Fields"
Cohesion: 0.18
Nodes (11): 1. Domain Entities & Value Types, 2. Entity Descriptions & Fields, 3. JSON Schema Mapping, `ArticleSelectionResult` (Dataclass), `BatchProcessingResult` (Dataclass), `CandidateStatus` (Enum), Data Model: Deterministic Content Selection, Entrada (+3 more)
### Community 125 - "Implementation Plan: Deterministic Article Content Selection"
Cohesion: 0.18
Nodes (11): Constitution Check, Documentation (this feature), Implementation Phases, Implementation Plan: Deterministic Article Content Selection, Phase 0: Outline & Research *(Completed)*, Phase 1: Design & Contracts *(Completed)*, Phase 2: Tasks & Execution *(Next Step via `/speckit-tasks`)*, Project Structure (+3 more)
### Community 126 - "Deterministic Content Selection Checklist: End-to-End Requirements Quality"
Cohesion: 0.25
Nodes (7): Candidate State Transitions & Resilience, Decision & Tie-Breaking Hierarchy, Deterministic Content Selection Checklist: End-to-End Requirements Quality, JSON Schema Integrity & Atomic I/O, Notes, Shingles & Consensus Metric Formulation, Text Normalization & Tokenization Quality
### Community 127 - "Quickstart: Deterministic Article Content Selection"
Cohesion: 0.29
Nodes (7): 1. Pré-requisitos, 2. Execução Rápida via CLI, 3. Execução dos Testes Automatizados, 4. Validação Programática / Uso como Módulo Python, Cenário 1: Selecionar o melhor extrator para uma extração existente, Cenário 2: Especificar caminho de saída customizado e modo verboso, Quickstart: Deterministic Article Content Selection
### Community 128 - "Specification Quality Checklist: Deterministic Content Selection"
Cohesion: 0.33
Nodes (5): Content Quality, Feature Readiness, Notes, Requirement Completeness, Specification Quality Checklist: Deterministic Content Selection
### Community 129 - "CLI Interface Contract: Deterministic Article Content Selection"
Cohesion: 0.33
Nodes (5): 1. Command Syntax, 2. Arguments and Flags, 3. Standard Streams (I/O), 4. Exit Codes, CLI Interface Contract: Deterministic Article Content Selection
### Community 131 - "convert_article_to_markdown.py"
Cohesion: 0.19
Nodes (17): _get_dict(), normalize_date(), normalize_list(), normalize_scalar(), Any, Interpreta datas ISO 8601 e RFC 2822 preservando fuso horário ou YYYY-MM-DD…, Valida se a URL é absoluta com protocolo http ou https e hostname não vazio., Retorna o dicionário associado à chave ou um dicionário vazio caso não seja… (+9 more)
### Community 132 - "8. Regras funcionais"
Cohesion: 0.13
Nodes (15): 8. Regras funcionais, RF-001 — Receber um único artigo, RF-002 — Respeitar o extrator selecionado, RF-003 — Resolver o corpo dentro do extrator selecionado, RF-004 — Selecionar metadados deterministicamente, RF-005 — Priorizar metadados por campo, RF-006 — Normalizar valores escalares, RF-007 — Normalizar listas (+7 more)
### Community 133 - "12. Critérios de aceite"
Cohesion: 0.14
Nodes (14): 12. Critérios de aceite, CA-001 — Trafilatura selecionada, CA-002 — Newspaper4k selecionado, CA-003 — Readability selecionado, CA-004 — Fallback dentro do extrator, CA-005 — Proibição de fallback de corpo entre extratores, CA-006 — Metadado vindo de outro extrator, CA-007 — Obrigatórios presentes (+6 more)
### Community 134 - "PRD — Conversão de artigo JSON para Markdown"
Cohesion: 0.17
Nodes (11): 10. Tratamento de erros, 14. Definition of Done, 15. Dependência técnica escolhida, 1. Visão geral, 2. Problema, 3. Objetivo, 4. História do usuário, 7.1 Arquivo (+3 more)
### Community 135 - "resolve_article_body"
Cohesion: 0.20
Nodes (10): Obtém o corpo do artigo exclusivamente do selected_extractor com fallback…, resolve_article_body(), Testa prioridade trafilatura.markdown sobre trafilatura.text., Testa prioridade newspaper4k.article_html sobre newspaper4k.text., Testa prioridade readability.cleaned_html sobre readability.cleaned_text., Garante erro ao receber selected_extractor ausente ou não reconhecido., test_resolve_article_body_invalid_selected_extractor(), test_resolve_article_body_newspaper4k_primary_and_fallback() (+2 more)
### Community 136 - "Implementation Plan: Convert Article JSON to Markdown"
Cohesion: 0.17
Nodes (12): Complexity Tracking, Constitution Check, Documentation (this feature), Implementation Phases, Implementation Plan: Convert Article JSON to Markdown, Phase 0: Outline & Research *(Completed)*, Phase 1: Design & Contracts *(Completed)*, Phase 2: Tasks & Implementation Breakdown *(Next: `/speckit-tasks`)* (+4 more)
### Community 137 - "2. Technical Decisions & Research Findings"
Cohesion: 0.17
Nodes (11): 1. Executive Summary & Goals, 2. Technical Decisions & Research Findings, 3. Technology Stack & Dependencies, Decision 1: HTML-to-Markdown Engine Selection, Decision 2: Direct Markdown Handling for Trafilatura, Decision 3: Metadata Normalization & Priority Resolution Pipeline, Decision 4: Date Parsing Strategy (ISO 8601 & RFC 2822), Decision 5: URL Validation & Media Filtering (+3 more)
### Community 138 - "Feature Specification: Convert Article JSON to Markdown"
Cohesion: 0.17
Nodes (12): Assumptions, Edge Cases, Feature Specification: Convert Article JSON to Markdown, Functional Requirements, Key Entities, Measurable Outcomes, Requirements *(mandatory)*, Success Criteria *(mandatory)* (+4 more)
### Community 139 - "Markdown Conversion Checklist: End-to-End Requirements Quality"
Cohesion: 0.18
Nodes (11): 1. Validação de Entrada, Tipagem & Isolamento de Lotes, 2. Isolamento Estrito de Extrator & Conversão de Conteúdo (HTML/MD), 3. Resolução Determinística de Metadados & Mapeamento de SELECIONADO, 4. Normalização de Escalares, Placeholders & Sanitização de Listas, 5. Normalização de Datas, Fusos & Validação Estrita de URLs, 6. Sanitização Editorial, Tratamento de Imagens & Título Duplicado, 7. Estrutura, Sintaxe do Markdown de Saída & Restrições, 8. Interface CLI, Tratamento de Erros & Atomicidade (+3 more)
### Community 140 - "convert_article"
Cohesion: 0.15
Nodes (13): assemble_markdown_document(), clean_body_images(), convert_article(), Path, Preserva imagens com URL absoluta http/https, remove relativas/data:/vazias e…, Monta a estrutura final do documento Markdown respeitando a ordem estrita do…, Executa a leitura do JSON, validação, conversão e escrita atômica do arquivo…, Valida descarte de data:, relativos e deduplicação mantendo a primeira… (+5 more)
### Community 141 - "005-convert-json-markdown/plan.md"
Cohesion: 0.33
Nodes (3): 1. Output Document Specification, 2. Formatting & Syntax Constraints, Markdown Schema Contract: Output Article Markdown
### Community 142 - "Quickstart: Convert Article JSON to Markdown"
Cohesion: 0.22
Nodes (8): 1. Prerequisites & Setup, 2. Running the CLI Tool, 3. Verification & Testing, Basic Conversion (Default Output Path), Custom Destination Path, Quickstart: Convert Article JSON to Markdown, Run All Unit & Integration Tests, Run Linter & Type Checker
### Community 143 - "1. Domain Entities & Schemas"
Cohesion: 0.25
Nodes (8): 1. Domain Entities & Schemas, 2. Priority Resolution Matrix, Data Model: Convert Article JSON to Markdown, Entity 1: `ArticleInput` (Source JSON), Entity 2: `ExtractorBlock` (Per-Extractor Data), Entity 3: `ResolvedArticleMetadata`, Entity 4: `MarkdownDocument`, Validation Rules:
### Community 144 - "11. Requisitos não funcionais"
Cohesion: 0.29
Nodes (7): 11. Requisitos não funcionais, RNF-001 — Determinismo, RNF-002 — Compatibilidade, RNF-003 — Execução local, RNF-004 — Integridade, RNF-005 — Manutenibilidade, RNF-006 — Qualidade
### Community 145 - "parse_arguments"
Cohesion: 0.29
Nodes (7): main(), parse_arguments(), Namespace, Configura o parser de argumentos do CLI., Ponto de entrada do CLI., Testa a chamada direta do parse_arguments., test_parse_arguments_api_direct()
### Community 146 - "Specification Quality Checklist: Convert Article JSON to Markdown"
Cohesion: 0.33
Nodes (5): Content Quality, Feature Readiness, Notes, Requirement Completeness, Specification Quality Checklist: Convert Article JSON to Markdown
### Community 147 - "CLI Contract: `convert_article_to_markdown.py`"
Cohesion: 0.33
Nodes (5): 1. Script Signature, 2. Command-Line Arguments, 3. Exit Codes, 4. Standard Stream Behavior, CLI Contract: `convert_article_to_markdown.py`
### Community 148 - "9. Interface CLI"
Cohesion: 0.40
Nodes (5): 9.1 Script, 9.2 Argumentos, 9.3 Exemplos, 9.4 Saída do processo, 9. Interface CLI
### Community 149 - "sample_rss_xml"
Cohesion: 0.67
Nodes (3): fixture, Fixture que fornece o conteúdo do XML de exemplo para testes offline., sample_rss_xml()
### Community 150 - "13. Estratégia de testes"
Cohesion: 0.50
Nodes (4): 13.1 Testes unitários, 13.2 Testes de integração do CLI, 13.3 Casos de resultado esperado, 13. Estratégia de testes
### Community 151 - "6. Contrato de entrada"
Cohesion: 0.50
Nodes (4): 6.1 Formato, 6.2 Valores aceitos para `selected_extractor`, 6.3 Campos obrigatórios após a resolução, 6. Contrato de entrada
### Community 152 - "ECPSnapshot"
Cohesion: 0.12
Nodes (21): ECPSnapshot, test_classifier_with_adapter_flags(), ecp_tech_corp(), fixture, Suíte de Testes para o Adaptador de Fallback para LLM (Tier 3) do Classificador…, Valida extração de JSON quando a resposta do LLM vem formatada em bloco…, Valida que respostas corrompidas ou JSONs sem campos obrigatórios retornem None…, Garante que o classificador dispare o Tier 3 LLM para casos ambíguos… (+13 more)
### Community 153 - "convert_html_to_markdown"
Cohesion: 0.33
Nodes (6): convert_html_to_markdown(), Converte HTML para Markdown usando títulos ATX, removendo scripts e estilos., Valida conversão de elementos HTML estruturados para Markdown com títulos ATX., Testa conversão de HTML vazio retornando string vazia., test_convert_html_to_markdown_empty_or_whitespace(), test_convert_html_to_markdown_rich_formatting()
### Community 154 - "JSON Schema Contract: Deterministic Article Content Selection"
Cohesion: 0.50
Nodes (3): 1. Input JSON Schema, 2. Output JSON Schema, JSON Schema Contract: Deterministic Article Content Selection
### Community 155 - "5. Escopo"
Cohesion: 0.67
Nodes (3): 5.1 Incluído, 5.2 Fora do escopo, 5. Escopo
### Community 165 - "test_e2e_text_analysis_pipeline.py"
Cohesion: 0.10
Nodes (19): ecp_river_plate(), fixture, parametrize, Suíte de Testes E2E e de Integração Completa para Análise de Texto e…, Cenário 1: Artigo com alta densidade de âncoras do River Plate. Oráculo:…, Cenário 2: Artigo sobre a Bacia do Rio da Prata ou clube homônimo do Uruguai.…, Cenário 3: Menção isolada do clube ('River') em contexto com poucas âncoras…, Cenário 4: Menção metafórica ou turística a um local próximo. Tier 1… (+11 more)
### Community 166 - "remove_duplicate_initial_h1"
Cohesion: 0.50
Nodes (4): Remove o primeiro título H1 do corpo somente quando ele for igual ao título…, remove_duplicate_initial_h1(), Testa remoção de H1 inicial coincidente com título com variações de espaços e…, test_remove_duplicate_initial_h1_exact_and_variations()
### Community 168 - "LLMFallbackAdapter"
Cohesion: 0.10
Nodes (16): LLMFallbackAdapter, Executes LLM fallback for ambiguous boundary cases. Returns a refined…, Parses and validates structured JSON response from LLM., Optional adapter for LLM fallback boundary disambiguation., Returns True if an API key or custom provider function is configured., Constructs an expert-engineered prompt for multilingual entity inherence…, Any, Unit tests for optional adapter interfaces (Tier 2 / Tier 3). (+8 more)
### Community 169 - "test_funnel_cli_subprocess_end_to_end"
Cohesion: 0.67
Nodes (3): Path, Valida o contrato CLI completo classify.py com saída em arquivo JSON e flags…, test_funnel_cli_subprocess_end_to_end()
### Community 170 - "test_models.py"
Cohesion: 0.16
Nodes (9): ClassificationError, Any, parametrize, test_benchmark_case(), Unit tests for ECP models, schema validation, and structured error handling., test_classification_error_serialization(), test_ecp_snapshot_defaults(), test_ecp_snapshot_missing_required() (+1 more)
### Community 171 - "extract_evidence_snippets"
Cohesion: 0.24
Nodes (9): extract_evidence_snippets(), extract_sentences(), Markdown content parser and excerpt extraction utilities., Split text into individual sentences., Extract relevant sentence excerpts from Markdown text that contain any of the…, Remove markdown syntax markers (headers, bold, italics, links, code blocks) to…, strip_markdown(), test_extract_evidence_snippets() (+1 more)
### Community 172 - ".classify"
Cohesion: 0.28
Nodes (8): count_phrase_occurrences(), match_phrase_in_text(), Check if a normalized phrase appears in normalized text with word boundary…, Count occurrences of a phrase in text., Classify inherence of content against an ECP snapshot., normalize_text(), Normalize text by converting to lowercase and stripping combining diacritical…, test_normalize_text()
### Community 173 - "🧪 Documentação da Suíte de Testes Automatizados"
Cohesion: 0.29
Nodes (6): 1. Executar Toda a Suíte do Projeto (247 testes), 2. Executar por Módulo Específico, 🚀 Como Executar os Testes, 🔑 Configuração para Testes com LLM ao Vivo, 🧪 Documentação da Suíte de Testes Automatizados, 📊 Inventário e Mapa de Cobertura das Suítes
## Knowledge Gaps ## Knowledge Gaps
- **465 isolated node(s):** `text-nlp-classifier`, `MatchedGraphEntity`, `graphify`, `Usage`, `What graphify is for` (+460 more) - **700 isolated node(s):** `text-nlp-classifier`, `MatchedGraphEntity`, `graphify`, `Usage`, `What graphify is for` (+695 more)
These have ≤1 connection - possible missing edges or undocumented components. These have ≤1 connection - possible missing edges or undocumented components.
- **38 thin communities (<3 nodes) omitted from report** — run `graphify query` to explore isolated nodes. - **48 thin communities (<3 nodes) omitted from report** — run `graphify query` to explore isolated nodes.
## Suggested Questions ## Suggested Questions
_Questions this graph is uniquely positioned to answer:_ _Questions this graph is uniquely positioned to answer:_
- **Why does `ECPSnapshot` connect `ECPSnapshot` to `main`, `models.py`, `classifier.py`, `InherenceClassifier`, `detect_language`?** - **Why does `ECPSnapshot` connect `ECPSnapshot` to `main`, `test_e2e_text_analysis_pipeline.py`, `LLMFallbackAdapter`, `test_models.py`, `.classify`, `ClassificationResult`, `test_adversarial.py`, `InherenceClassifier`?**
_High betweenness centrality (0.006) - this node is a cross-community bridge._ _High betweenness centrality (0.008) - this node is a cross-community bridge._
- **Why does `InherenceClassifier` connect `InherenceClassifier` to `main`, `ECPSnapshot`, `models.py`, `classifier.py`, `detect_language`?** - **Why does `PRD — Conversão de artigo JSON para Markdown` connect `PRD — Conversão de artigo JSON para Markdown` to `8. Regras funcionais`, `12. Critérios de aceite`, `11. Requisitos não funcionais`, `9. Interface CLI`, `13. Estratégia de testes`, `6. Contrato de entrada`, `5. Escopo`?**
_High betweenness centrality (0.003) - this node is a cross-community bridge._ _High betweenness centrality (0.004) - this node is a cross-community bridge._
- **Why does `detect_language()` connect `detect_language` to `classifier.py`?** - **Why does `InherenceClassifier` connect `InherenceClassifier` to `main`, `test_e2e_text_analysis_pipeline.py`, `LLMFallbackAdapter`, `.classify`, `ClassificationResult`, `test_adversarial.py`, `ECPSnapshot`?**
_High betweenness centrality (0.003) - this node is a cross-community bridge._ _High betweenness centrality (0.004) - this node is a cross-community bridge._
- **Are the 10 inferred relationships involving `ECPSnapshot` (e.g. with `main()` and `BaseNLPAdapter`) actually correct?** - **Are the 42 inferred relationships involving `ECPSnapshot` (e.g. with `main()` and `BaseNLPAdapter`) actually correct?**
_`ECPSnapshot` has 10 INFERRED edges - model-reasoned connections that need verification._ _`ECPSnapshot` has 42 INFERRED edges - model-reasoned connections that need verification._
- **Are the 6 inferred relationships involving `InherenceClassifier` (e.g. with `LocalEmbeddingsAdapter` and `LLMFallbackAdapter`) actually correct?** - **Are the 8 inferred relationships involving `InherenceClassifier` (e.g. with `LocalEmbeddingsAdapter` and `LLMFallbackAdapter`) actually correct?**
_`InherenceClassifier` has 6 INFERRED edges - model-reasoned connections that need verification._ _`InherenceClassifier` has 8 INFERRED edges - model-reasoned connections that need verification._
- **Are the 10 inferred relationships involving `DecisionCategory` (e.g. with `InherenceClassifier` and `test_adversarial_apple_fruit_recipe()`) actually correct?** - **Are the 50 inferred relationships involving `DecisionCategory` (e.g. with `LLMFallbackAdapter` and `InherenceClassifier`) actually correct?**
_`DecisionCategory` has 10 INFERRED edges - model-reasoned connections that need verification._ _`DecisionCategory` has 50 INFERRED edges - model-reasoned connections that need verification._
- **Are the 4 inferred relationships involving `ClassificationResult` (e.g. with `BaseNLPAdapter` and `LocalEmbeddingsAdapter`) actually correct?** - **Are the 4 inferred relationships involving `LLMFallbackAdapter` (e.g. with `ClassificationResult` and `DecisionCategory`) actually correct?**
_`ClassificationResult` has 4 INFERRED edges - model-reasoned connections that need verification._ _`LLMFallbackAdapter` has 4 INFERRED edges - model-reasoned connections that need verification._
@@ -0,0 +1 @@
{"nodes": [{"id": "$graphify-root$_tests_fixtures_markdown_conversion_valid_trafilatura_md", "label": "valid_trafilatura.md", "file_type": "document", "node_kind": "page", "source_file": "tests/fixtures/markdown_conversion/valid_trafilatura.md", "source_location": "L1"}, {"id": "$graphify-root$_tests_fixtures_markdown_conversion_valid_trafilatura_los_puntajes_de_river_vs_independiente_santa_fe_por_la_copa_sudamericana_tyc_sports", "label": "Los puntajes de River vs. Independiente Santa Fe, por la Copa Sudamericana - TyC Sports", "file_type": "document", "node_kind": "heading", "source_file": "tests/fixtures/markdown_conversion/valid_trafilatura.md", "source_location": "L1"}, {"id": "$graphify-root$_tests_fixtures_markdown_conversion_valid_trafilatura_santiago_beltr\u00e1n_6", "label": "SANTIAGO BELTR\u00c1N - 6", "file_type": "document", "node_kind": "heading", "source_file": "tests/fixtures/markdown_conversion/valid_trafilatura.md", "source_location": "L19"}], "edges": [{"source": "$graphify-root$_tests_fixtures_markdown_conversion_valid_trafilatura_md", "target": "$graphify-root$_tests_fixtures_markdown_conversion_valid_trafilatura_los_puntajes_de_river_vs_independiente_santa_fe_por_la_copa_sudamericana_tyc_sports", "relation": "contains", "confidence": "EXTRACTED", "source_file": "tests/fixtures/markdown_conversion/valid_trafilatura.md", "source_location": "L1", "weight": 1.0}, {"source": "$graphify-root$_tests_fixtures_markdown_conversion_valid_trafilatura_los_puntajes_de_river_vs_independiente_santa_fe_por_la_copa_sudamericana_tyc_sports", "target": "$graphify-root$_tests_fixtures_markdown_conversion_valid_trafilatura_santiago_beltr\u00e1n_6", "relation": "contains", "confidence": "EXTRACTED", "source_file": "tests/fixtures/markdown_conversion/valid_trafilatura.md", "source_location": "L19", "weight": 1.0}], "input_tokens": 0, "output_tokens": 0}
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
{"nodes": [{"id": "$graphify-root$_tests_fixtures_markdown_conversion_valid_readability_md", "label": "valid_readability.md", "file_type": "document", "node_kind": "page", "source_file": "tests/fixtures/markdown_conversion/valid_readability.md", "source_location": "L1"}, {"id": "$graphify-root$_tests_fixtures_markdown_conversion_valid_readability_an\u00e1lisis_t\u00e1ctico_del_partido_de_river_en_bogot\u00e1", "label": "An\u00e1lisis t\u00e1ctico del partido de River en Bogot\u00e1", "file_type": "document", "node_kind": "heading", "source_file": "tests/fixtures/markdown_conversion/valid_readability.md", "source_location": "L1"}], "edges": [{"source": "$graphify-root$_tests_fixtures_markdown_conversion_valid_readability_md", "target": "$graphify-root$_tests_fixtures_markdown_conversion_valid_readability_an\u00e1lisis_t\u00e1ctico_del_partido_de_river_en_bogot\u00e1", "relation": "contains", "confidence": "EXTRACTED", "source_file": "tests/fixtures/markdown_conversion/valid_readability.md", "source_location": "L1", "weight": 1.0}], "input_tokens": 0, "output_tokens": 0}
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
{"nodes": [{"id": "$graphify-root$_tests_fixtures_markdown_conversion_valid_newspaper4k_md", "label": "valid_newspaper4k.md", "file_type": "document", "node_kind": "page", "source_file": "tests/fixtures/markdown_conversion/valid_newspaper4k.md", "source_location": "L1"}, {"id": "$graphify-root$_tests_fixtures_markdown_conversion_valid_newspaper4k_qui\u00e9n_es_paz_zubiri_la_relatora_de_fox_sports", "label": "Qui\u00e9n es Paz Zubiri, la relatora de Fox Sports", "file_type": "document", "node_kind": "heading", "source_file": "tests/fixtures/markdown_conversion/valid_newspaper4k.md", "source_location": "L1"}], "edges": [{"source": "$graphify-root$_tests_fixtures_markdown_conversion_valid_newspaper4k_md", "target": "$graphify-root$_tests_fixtures_markdown_conversion_valid_newspaper4k_qui\u00e9n_es_paz_zubiri_la_relatora_de_fox_sports", "relation": "contains", "confidence": "EXTRACTED", "source_file": "tests/fixtures/markdown_conversion/valid_newspaper4k.md", "source_location": "L1", "weight": 1.0}], "input_tokens": 0, "output_tokens": 0}
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
{"nodes": [{"id": "$graphify-root$_specs_004_deterministic_content_selection_contracts_cli_contract_md", "label": "cli-contract.md", "file_type": "document", "node_kind": "page", "source_file": "specs/004-deterministic-content-selection/contracts/cli-contract.md", "source_location": "L1"}, {"id": "$graphify-root$_specs_004_deterministic_content_selection_contracts_cli_contract_cli_interface_contract_deterministic_article_content_selection", "label": "CLI Interface Contract: Deterministic Article Content Selection", "file_type": "document", "node_kind": "heading", "source_file": "specs/004-deterministic-content-selection/contracts/cli-contract.md", "source_location": "L1"}, {"id": "$graphify-root$_specs_004_deterministic_content_selection_contracts_cli_contract_1_command_syntax", "label": "1. Command Syntax", "file_type": "document", "node_kind": "heading", "source_file": "specs/004-deterministic-content-selection/contracts/cli-contract.md", "source_location": "L7"}, {"id": "$graphify-root$_specs_004_deterministic_content_selection_contracts_cli_contract_2_arguments_and_flags", "label": "2. Arguments and Flags", "file_type": "document", "node_kind": "heading", "source_file": "specs/004-deterministic-content-selection/contracts/cli-contract.md", "source_location": "L15"}, {"id": "$graphify-root$_specs_004_deterministic_content_selection_contracts_cli_contract_3_standard_streams_i_o", "label": "3. Standard Streams (I/O)", "file_type": "document", "node_kind": "heading", "source_file": "specs/004-deterministic-content-selection/contracts/cli-contract.md", "source_location": "L26"}, {"id": "$graphify-root$_specs_004_deterministic_content_selection_contracts_cli_contract_4_exit_codes", "label": "4. Exit Codes", "file_type": "document", "node_kind": "heading", "source_file": "specs/004-deterministic-content-selection/contracts/cli-contract.md", "source_location": "L48"}], "edges": [{"source": "$graphify-root$_specs_004_deterministic_content_selection_contracts_cli_contract_md", "target": "$graphify-root$_specs_004_deterministic_content_selection_contracts_cli_contract_cli_interface_contract_deterministic_article_content_selection", "relation": "contains", "confidence": "EXTRACTED", "source_file": "specs/004-deterministic-content-selection/contracts/cli-contract.md", "source_location": "L1", "weight": 1.0}, {"source": "$graphify-root$_specs_004_deterministic_content_selection_contracts_cli_contract_md", "target": "$graphify-root$_specs_004_deterministic_content_selection_spec_md", "relation": "references", "confidence": "EXTRACTED", "source_file": "specs/004-deterministic-content-selection/contracts/cli-contract.md", "source_location": "L3", "weight": 1.0, "target_file": "$graphify-root$/specs/004-deterministic-content-selection/spec.md"}, {"source": "$graphify-root$_specs_004_deterministic_content_selection_contracts_cli_contract_cli_interface_contract_deterministic_article_content_selection", "target": "$graphify-root$_specs_004_deterministic_content_selection_contracts_cli_contract_1_command_syntax", "relation": "contains", "confidence": "EXTRACTED", "source_file": "specs/004-deterministic-content-selection/contracts/cli-contract.md", "source_location": "L7", "weight": 1.0}, {"source": "$graphify-root$_specs_004_deterministic_content_selection_contracts_cli_contract_cli_interface_contract_deterministic_article_content_selection", "target": "$graphify-root$_specs_004_deterministic_content_selection_contracts_cli_contract_2_arguments_and_flags", "relation": "contains", "confidence": "EXTRACTED", "source_file": "specs/004-deterministic-content-selection/contracts/cli-contract.md", "source_location": "L15", "weight": 1.0}, {"source": "$graphify-root$_specs_004_deterministic_content_selection_contracts_cli_contract_cli_interface_contract_deterministic_article_content_selection", "target": "$graphify-root$_specs_004_deterministic_content_selection_contracts_cli_contract_3_standard_streams_i_o", "relation": "contains", "confidence": "EXTRACTED", "source_file": "specs/004-deterministic-content-selection/contracts/cli-contract.md", "source_location": "L26", "weight": 1.0}, {"source": "$graphify-root$_specs_004_deterministic_content_selection_contracts_cli_contract_cli_interface_contract_deterministic_article_content_selection", "target": "$graphify-root$_specs_004_deterministic_content_selection_contracts_cli_contract_4_exit_codes", "relation": "contains", "confidence": "EXTRACTED", "source_file": "specs/004-deterministic-content-selection/contracts/cli-contract.md", "source_location": "L48", "weight": 1.0}], "input_tokens": 0, "output_tokens": 0}
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
{"nodes": [{"id": "$graphify-root$_tests_readme_md", "label": "README.md", "file_type": "document", "node_kind": "page", "source_file": "tests/README.md", "source_location": "L1"}, {"id": "$graphify-root$_tests_readme_documenta\u00e7\u00e3o_da_su\u00edte_de_testes_automatizados", "label": "\ud83e\uddea Documenta\u00e7\u00e3o da Su\u00edte de Testes Automatizados", "file_type": "document", "node_kind": "heading", "source_file": "tests/README.md", "source_location": "L1"}, {"id": "$graphify-root$_tests_readme_invent\u00e1rio_e_mapa_de_cobertura_das_su\u00edtes", "label": "\ud83d\udcca Invent\u00e1rio e Mapa de Cobertura das Su\u00edtes", "file_type": "document", "node_kind": "heading", "source_file": "tests/README.md", "source_location": "L7"}, {"id": "$graphify-root$_tests_readme_como_executar_os_testes", "label": "\ud83d\ude80 Como Executar os Testes", "file_type": "document", "node_kind": "heading", "source_file": "tests/README.md", "source_location": "L29"}, {"id": "$graphify-root$_tests_readme_1_executar_toda_a_su\u00edte_do_projeto_247_testes", "label": "1. Executar Toda a Su\u00edte do Projeto (247 testes)", "file_type": "document", "node_kind": "heading", "source_file": "tests/README.md", "source_location": "L31"}, {"id": "$graphify-root$_tests_readme_2_executar_por_m\u00f3dulo_espec\u00edfico", "label": "2. Executar por M\u00f3dulo Espec\u00edfico", "file_type": "document", "node_kind": "heading", "source_file": "tests/README.md", "source_location": "L36"}, {"id": "$graphify-root$_tests_readme_configura\u00e7\u00e3o_para_testes_com_llm_ao_vivo", "label": "\ud83d\udd11 Configura\u00e7\u00e3o para Testes com LLM ao Vivo", "file_type": "document", "node_kind": "heading", "source_file": "tests/README.md", "source_location": "L63"}], "edges": [{"source": "$graphify-root$_tests_readme_md", "target": "$graphify-root$_tests_readme_documenta\u00e7\u00e3o_da_su\u00edte_de_testes_automatizados", "relation": "contains", "confidence": "EXTRACTED", "source_file": "tests/README.md", "source_location": "L1", "weight": 1.0}, {"source": "$graphify-root$_tests_readme_documenta\u00e7\u00e3o_da_su\u00edte_de_testes_automatizados", "target": "$graphify-root$_tests_readme_invent\u00e1rio_e_mapa_de_cobertura_das_su\u00edtes", "relation": "contains", "confidence": "EXTRACTED", "source_file": "tests/README.md", "source_location": "L7", "weight": 1.0}, {"source": "$graphify-root$_tests_readme_documenta\u00e7\u00e3o_da_su\u00edte_de_testes_automatizados", "target": "$graphify-root$_tests_readme_como_executar_os_testes", "relation": "contains", "confidence": "EXTRACTED", "source_file": "tests/README.md", "source_location": "L29", "weight": 1.0}, {"source": "$graphify-root$_tests_readme_como_executar_os_testes", "target": "$graphify-root$_tests_readme_1_executar_toda_a_su\u00edte_do_projeto_247_testes", "relation": "contains", "confidence": "EXTRACTED", "source_file": "tests/README.md", "source_location": "L31", "weight": 1.0}, {"source": "$graphify-root$_tests_readme_como_executar_os_testes", "target": "$graphify-root$_tests_readme_2_executar_por_m\u00f3dulo_espec\u00edfico", "relation": "contains", "confidence": "EXTRACTED", "source_file": "tests/README.md", "source_location": "L36", "weight": 1.0}, {"source": "$graphify-root$_tests_readme_documenta\u00e7\u00e3o_da_su\u00edte_de_testes_automatizados", "target": "$graphify-root$_tests_readme_configura\u00e7\u00e3o_para_testes_com_llm_ao_vivo", "relation": "contains", "confidence": "EXTRACTED", "source_file": "tests/README.md", "source_location": "L63", "weight": 1.0}], "input_tokens": 0, "output_tokens": 0}
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
{"nodes": [{"id": "$graphify-root$_specs_004_deterministic_content_selection_contracts_cli_contract_md", "label": "cli-contract.md", "file_type": "document", "node_kind": "page", "source_file": "specs/004-deterministic-content-selection/contracts/cli-contract.md", "source_location": "L1"}, {"id": "$graphify-root$_specs_004_deterministic_content_selection_contracts_cli_contract_cli_interface_contract_deterministic_article_content_selection", "label": "CLI Interface Contract: Deterministic Article Content Selection", "file_type": "document", "node_kind": "heading", "source_file": "specs/004-deterministic-content-selection/contracts/cli-contract.md", "source_location": "L1"}, {"id": "$graphify-root$_specs_004_deterministic_content_selection_contracts_cli_contract_1_command_syntax", "label": "1. Command Syntax", "file_type": "document", "node_kind": "heading", "source_file": "specs/004-deterministic-content-selection/contracts/cli-contract.md", "source_location": "L7"}, {"id": "$graphify-root$_specs_004_deterministic_content_selection_contracts_cli_contract_2_arguments_and_flags", "label": "2. Arguments and Flags", "file_type": "document", "node_kind": "heading", "source_file": "specs/004-deterministic-content-selection/contracts/cli-contract.md", "source_location": "L15"}, {"id": "$graphify-root$_specs_004_deterministic_content_selection_contracts_cli_contract_3_standard_streams_i_o", "label": "3. Standard Streams (I/O)", "file_type": "document", "node_kind": "heading", "source_file": "specs/004-deterministic-content-selection/contracts/cli-contract.md", "source_location": "L26"}, {"id": "$graphify-root$_specs_004_deterministic_content_selection_contracts_cli_contract_4_exit_codes", "label": "4. Exit Codes", "file_type": "document", "node_kind": "heading", "source_file": "specs/004-deterministic-content-selection/contracts/cli-contract.md", "source_location": "L48"}], "edges": [{"source": "$graphify-root$_specs_004_deterministic_content_selection_contracts_cli_contract_md", "target": "$graphify-root$_specs_004_deterministic_content_selection_contracts_cli_contract_cli_interface_contract_deterministic_article_content_selection", "relation": "contains", "confidence": "EXTRACTED", "source_file": "specs/004-deterministic-content-selection/contracts/cli-contract.md", "source_location": "L1", "weight": 1.0}, {"source": "$graphify-root$_specs_004_deterministic_content_selection_contracts_cli_contract_md", "target": "$graphify-root$_specs_004_deterministic_content_selection_spec_md", "relation": "references", "confidence": "EXTRACTED", "source_file": "specs/004-deterministic-content-selection/contracts/cli-contract.md", "source_location": "L3", "weight": 1.0, "target_file": "$graphify-root$/specs/004-deterministic-content-selection/spec.md"}, {"source": "$graphify-root$_specs_004_deterministic_content_selection_contracts_cli_contract_cli_interface_contract_deterministic_article_content_selection", "target": "$graphify-root$_specs_004_deterministic_content_selection_contracts_cli_contract_1_command_syntax", "relation": "contains", "confidence": "EXTRACTED", "source_file": "specs/004-deterministic-content-selection/contracts/cli-contract.md", "source_location": "L7", "weight": 1.0}, {"source": "$graphify-root$_specs_004_deterministic_content_selection_contracts_cli_contract_cli_interface_contract_deterministic_article_content_selection", "target": "$graphify-root$_specs_004_deterministic_content_selection_contracts_cli_contract_2_arguments_and_flags", "relation": "contains", "confidence": "EXTRACTED", "source_file": "specs/004-deterministic-content-selection/contracts/cli-contract.md", "source_location": "L15", "weight": 1.0}, {"source": "$graphify-root$_specs_004_deterministic_content_selection_contracts_cli_contract_cli_interface_contract_deterministic_article_content_selection", "target": "$graphify-root$_specs_004_deterministic_content_selection_contracts_cli_contract_3_standard_streams_i_o", "relation": "contains", "confidence": "EXTRACTED", "source_file": "specs/004-deterministic-content-selection/contracts/cli-contract.md", "source_location": "L26", "weight": 1.0}, {"source": "$graphify-root$_specs_004_deterministic_content_selection_contracts_cli_contract_cli_interface_contract_deterministic_article_content_selection", "target": "$graphify-root$_specs_004_deterministic_content_selection_contracts_cli_contract_4_exit_codes", "relation": "contains", "confidence": "EXTRACTED", "source_file": "specs/004-deterministic-content-selection/contracts/cli-contract.md", "source_location": "L48", "weight": 1.0}], "input_tokens": 0, "output_tokens": 0}
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
{"nodes": [{"id": "$graphify-root$_specs_005_convert_json_markdown_checklists_requirements_md", "label": "requirements.md", "file_type": "document", "node_kind": "page", "source_file": "specs/005-convert-json-markdown/checklists/requirements.md", "source_location": "L1"}, {"id": "$graphify-root$_specs_005_convert_json_markdown_checklists_requirements_specification_quality_checklist_convert_article_json_to_markdown", "label": "Specification Quality Checklist: Convert Article JSON to Markdown", "file_type": "document", "node_kind": "heading", "source_file": "specs/005-convert-json-markdown/checklists/requirements.md", "source_location": "L1"}, {"id": "$graphify-root$_specs_005_convert_json_markdown_checklists_requirements_content_quality", "label": "Content Quality", "file_type": "document", "node_kind": "heading", "source_file": "specs/005-convert-json-markdown/checklists/requirements.md", "source_location": "L7"}, {"id": "$graphify-root$_specs_005_convert_json_markdown_checklists_requirements_requirement_completeness", "label": "Requirement Completeness", "file_type": "document", "node_kind": "heading", "source_file": "specs/005-convert-json-markdown/checklists/requirements.md", "source_location": "L14"}, {"id": "$graphify-root$_specs_005_convert_json_markdown_checklists_requirements_feature_readiness", "label": "Feature Readiness", "file_type": "document", "node_kind": "heading", "source_file": "specs/005-convert-json-markdown/checklists/requirements.md", "source_location": "L25"}, {"id": "$graphify-root$_specs_005_convert_json_markdown_checklists_requirements_notes", "label": "Notes", "file_type": "document", "node_kind": "heading", "source_file": "specs/005-convert-json-markdown/checklists/requirements.md", "source_location": "L32"}], "edges": [{"source": "$graphify-root$_specs_005_convert_json_markdown_checklists_requirements_md", "target": "$graphify-root$_specs_005_convert_json_markdown_checklists_requirements_specification_quality_checklist_convert_article_json_to_markdown", "relation": "contains", "confidence": "EXTRACTED", "source_file": "specs/005-convert-json-markdown/checklists/requirements.md", "source_location": "L1", "weight": 1.0}, {"source": "$graphify-root$_specs_005_convert_json_markdown_checklists_requirements_md", "target": "$graphify-root$_specs_005_convert_json_markdown_spec_md", "relation": "references", "confidence": "EXTRACTED", "source_file": "specs/005-convert-json-markdown/checklists/requirements.md", "source_location": "L5", "weight": 1.0, "target_file": "$graphify-root$/specs/005-convert-json-markdown/spec.md"}, {"source": "$graphify-root$_specs_005_convert_json_markdown_checklists_requirements_specification_quality_checklist_convert_article_json_to_markdown", "target": "$graphify-root$_specs_005_convert_json_markdown_checklists_requirements_content_quality", "relation": "contains", "confidence": "EXTRACTED", "source_file": "specs/005-convert-json-markdown/checklists/requirements.md", "source_location": "L7", "weight": 1.0}, {"source": "$graphify-root$_specs_005_convert_json_markdown_checklists_requirements_specification_quality_checklist_convert_article_json_to_markdown", "target": "$graphify-root$_specs_005_convert_json_markdown_checklists_requirements_requirement_completeness", "relation": "contains", "confidence": "EXTRACTED", "source_file": "specs/005-convert-json-markdown/checklists/requirements.md", "source_location": "L14", "weight": 1.0}, {"source": "$graphify-root$_specs_005_convert_json_markdown_checklists_requirements_specification_quality_checklist_convert_article_json_to_markdown", "target": "$graphify-root$_specs_005_convert_json_markdown_checklists_requirements_feature_readiness", "relation": "contains", "confidence": "EXTRACTED", "source_file": "specs/005-convert-json-markdown/checklists/requirements.md", "source_location": "L25", "weight": 1.0}, {"source": "$graphify-root$_specs_005_convert_json_markdown_checklists_requirements_specification_quality_checklist_convert_article_json_to_markdown", "target": "$graphify-root$_specs_005_convert_json_markdown_checklists_requirements_notes", "relation": "contains", "confidence": "EXTRACTED", "source_file": "specs/005-convert-json-markdown/checklists/requirements.md", "source_location": "L32", "weight": 1.0}], "input_tokens": 0, "output_tokens": 0}
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
{"nodes": [{"id": "$graphify-root$_specs_004_deterministic_content_selection_checklists_requirements_md", "label": "requirements.md", "file_type": "document", "node_kind": "page", "source_file": "specs/004-deterministic-content-selection/checklists/requirements.md", "source_location": "L1"}, {"id": "$graphify-root$_specs_004_deterministic_content_selection_checklists_requirements_specification_quality_checklist_deterministic_content_selection", "label": "Specification Quality Checklist: Deterministic Content Selection", "file_type": "document", "node_kind": "heading", "source_file": "specs/004-deterministic-content-selection/checklists/requirements.md", "source_location": "L1"}, {"id": "$graphify-root$_specs_004_deterministic_content_selection_checklists_requirements_content_quality", "label": "Content Quality", "file_type": "document", "node_kind": "heading", "source_file": "specs/004-deterministic-content-selection/checklists/requirements.md", "source_location": "L7"}, {"id": "$graphify-root$_specs_004_deterministic_content_selection_checklists_requirements_requirement_completeness", "label": "Requirement Completeness", "file_type": "document", "node_kind": "heading", "source_file": "specs/004-deterministic-content-selection/checklists/requirements.md", "source_location": "L14"}, {"id": "$graphify-root$_specs_004_deterministic_content_selection_checklists_requirements_feature_readiness", "label": "Feature Readiness", "file_type": "document", "node_kind": "heading", "source_file": "specs/004-deterministic-content-selection/checklists/requirements.md", "source_location": "L25"}, {"id": "$graphify-root$_specs_004_deterministic_content_selection_checklists_requirements_notes", "label": "Notes", "file_type": "document", "node_kind": "heading", "source_file": "specs/004-deterministic-content-selection/checklists/requirements.md", "source_location": "L32"}], "edges": [{"source": "$graphify-root$_specs_004_deterministic_content_selection_checklists_requirements_md", "target": "$graphify-root$_specs_004_deterministic_content_selection_checklists_requirements_specification_quality_checklist_deterministic_content_selection", "relation": "contains", "confidence": "EXTRACTED", "source_file": "specs/004-deterministic-content-selection/checklists/requirements.md", "source_location": "L1", "weight": 1.0}, {"source": "$graphify-root$_specs_004_deterministic_content_selection_checklists_requirements_md", "target": "$graphify-root$_specs_004_deterministic_content_selection_spec_md", "relation": "references", "confidence": "EXTRACTED", "source_file": "specs/004-deterministic-content-selection/checklists/requirements.md", "source_location": "L5", "weight": 1.0, "target_file": "$graphify-root$/specs/004-deterministic-content-selection/spec.md"}, {"source": "$graphify-root$_specs_004_deterministic_content_selection_checklists_requirements_specification_quality_checklist_deterministic_content_selection", "target": "$graphify-root$_specs_004_deterministic_content_selection_checklists_requirements_content_quality", "relation": "contains", "confidence": "EXTRACTED", "source_file": "specs/004-deterministic-content-selection/checklists/requirements.md", "source_location": "L7", "weight": 1.0}, {"source": "$graphify-root$_specs_004_deterministic_content_selection_checklists_requirements_specification_quality_checklist_deterministic_content_selection", "target": "$graphify-root$_specs_004_deterministic_content_selection_checklists_requirements_requirement_completeness", "relation": "contains", "confidence": "EXTRACTED", "source_file": "specs/004-deterministic-content-selection/checklists/requirements.md", "source_location": "L14", "weight": 1.0}, {"source": "$graphify-root$_specs_004_deterministic_content_selection_checklists_requirements_specification_quality_checklist_deterministic_content_selection", "target": "$graphify-root$_specs_004_deterministic_content_selection_checklists_requirements_feature_readiness", "relation": "contains", "confidence": "EXTRACTED", "source_file": "specs/004-deterministic-content-selection/checklists/requirements.md", "source_location": "L25", "weight": 1.0}, {"source": "$graphify-root$_specs_004_deterministic_content_selection_checklists_requirements_specification_quality_checklist_deterministic_content_selection", "target": "$graphify-root$_specs_004_deterministic_content_selection_checklists_requirements_notes", "relation": "contains", "confidence": "EXTRACTED", "source_file": "specs/004-deterministic-content-selection/checklists/requirements.md", "source_location": "L32", "weight": 1.0}], "input_tokens": 0, "output_tokens": 0}
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
{"nodes": [{"id": "$graphify-root$_specs_005_convert_json_markdown_contracts_markdown_schema_md", "label": "markdown-schema.md", "file_type": "document", "node_kind": "page", "source_file": "specs/005-convert-json-markdown/contracts/markdown-schema.md", "source_location": "L1"}, {"id": "$graphify-root$_specs_005_convert_json_markdown_contracts_markdown_schema_markdown_schema_contract_output_article_markdown", "label": "Markdown Schema Contract: Output Article Markdown", "file_type": "document", "node_kind": "heading", "source_file": "specs/005-convert-json-markdown/contracts/markdown-schema.md", "source_location": "L1"}, {"id": "$graphify-root$_specs_005_convert_json_markdown_contracts_markdown_schema_1_output_document_specification", "label": "1. Output Document Specification", "file_type": "document", "node_kind": "heading", "source_file": "specs/005-convert-json-markdown/contracts/markdown-schema.md", "source_location": "L5"}, {"id": "$graphify-root$_specs_005_convert_json_markdown_contracts_markdown_schema_2_formatting_syntax_constraints", "label": "2. Formatting & Syntax Constraints", "file_type": "document", "node_kind": "heading", "source_file": "specs/005-convert-json-markdown/contracts/markdown-schema.md", "source_location": "L30"}], "edges": [{"source": "$graphify-root$_specs_005_convert_json_markdown_contracts_markdown_schema_md", "target": "$graphify-root$_specs_005_convert_json_markdown_contracts_markdown_schema_markdown_schema_contract_output_article_markdown", "relation": "contains", "confidence": "EXTRACTED", "source_file": "specs/005-convert-json-markdown/contracts/markdown-schema.md", "source_location": "L1", "weight": 1.0}, {"source": "$graphify-root$_specs_005_convert_json_markdown_contracts_markdown_schema_markdown_schema_contract_output_article_markdown", "target": "$graphify-root$_specs_005_convert_json_markdown_contracts_markdown_schema_1_output_document_specification", "relation": "contains", "confidence": "EXTRACTED", "source_file": "specs/005-convert-json-markdown/contracts/markdown-schema.md", "source_location": "L5", "weight": 1.0}, {"source": "$graphify-root$_specs_005_convert_json_markdown_contracts_markdown_schema_markdown_schema_contract_output_article_markdown", "target": "$graphify-root$_specs_005_convert_json_markdown_contracts_markdown_schema_2_formatting_syntax_constraints", "relation": "contains", "confidence": "EXTRACTED", "source_file": "specs/005-convert-json-markdown/contracts/markdown-schema.md", "source_location": "L30", "weight": 1.0}], "input_tokens": 0, "output_tokens": 0}
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
{"nodes": [{"id": "$graphify-root$_specs_004_deterministic_content_selection_contracts_json_schema_md", "label": "json-schema.md", "file_type": "document", "node_kind": "page", "source_file": "specs/004-deterministic-content-selection/contracts/json-schema.md", "source_location": "L1"}, {"id": "$graphify-root$_specs_004_deterministic_content_selection_contracts_json_schema_json_schema_contract_deterministic_article_content_selection", "label": "JSON Schema Contract: Deterministic Article Content Selection", "file_type": "document", "node_kind": "heading", "source_file": "specs/004-deterministic-content-selection/contracts/json-schema.md", "source_location": "L1"}, {"id": "$graphify-root$_specs_004_deterministic_content_selection_contracts_json_schema_1_input_json_schema", "label": "1. Input JSON Schema", "file_type": "document", "node_kind": "heading", "source_file": "specs/004-deterministic-content-selection/contracts/json-schema.md", "source_location": "L7"}, {"id": "$graphify-root$_specs_004_deterministic_content_selection_contracts_json_schema_2_output_json_schema", "label": "2. Output JSON Schema", "file_type": "document", "node_kind": "heading", "source_file": "specs/004-deterministic-content-selection/contracts/json-schema.md", "source_location": "L52"}], "edges": [{"source": "$graphify-root$_specs_004_deterministic_content_selection_contracts_json_schema_md", "target": "$graphify-root$_specs_004_deterministic_content_selection_contracts_json_schema_json_schema_contract_deterministic_article_content_selection", "relation": "contains", "confidence": "EXTRACTED", "source_file": "specs/004-deterministic-content-selection/contracts/json-schema.md", "source_location": "L1", "weight": 1.0}, {"source": "$graphify-root$_specs_004_deterministic_content_selection_contracts_json_schema_md", "target": "$graphify-root$_specs_004_deterministic_content_selection_spec_md", "relation": "references", "confidence": "EXTRACTED", "source_file": "specs/004-deterministic-content-selection/contracts/json-schema.md", "source_location": "L3", "weight": 1.0, "target_file": "$graphify-root$/specs/004-deterministic-content-selection/spec.md"}, {"source": "$graphify-root$_specs_004_deterministic_content_selection_contracts_json_schema_json_schema_contract_deterministic_article_content_selection", "target": "$graphify-root$_specs_004_deterministic_content_selection_contracts_json_schema_1_input_json_schema", "relation": "contains", "confidence": "EXTRACTED", "source_file": "specs/004-deterministic-content-selection/contracts/json-schema.md", "source_location": "L7", "weight": 1.0}, {"source": "$graphify-root$_specs_004_deterministic_content_selection_contracts_json_schema_json_schema_contract_deterministic_article_content_selection", "target": "$graphify-root$_specs_004_deterministic_content_selection_contracts_json_schema_2_output_json_schema", "relation": "contains", "confidence": "EXTRACTED", "source_file": "specs/004-deterministic-content-selection/contracts/json-schema.md", "source_location": "L52", "weight": 1.0}], "input_tokens": 0, "output_tokens": 0}
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
{"nodes": [{"id": "$graphify-root$_specs_005_convert_json_markdown_contracts_cli_contract_md", "label": "cli-contract.md", "file_type": "document", "node_kind": "page", "source_file": "specs/005-convert-json-markdown/contracts/cli-contract.md", "source_location": "L1"}, {"id": "$graphify-root$_specs_005_convert_json_markdown_contracts_cli_contract_cli_contract_convert_article_to_markdown_py", "label": "CLI Contract: `convert_article_to_markdown.py`", "file_type": "document", "node_kind": "heading", "source_file": "specs/005-convert-json-markdown/contracts/cli-contract.md", "source_location": "L1"}, {"id": "$graphify-root$_specs_005_convert_json_markdown_contracts_cli_contract_1_script_signature", "label": "1. Script Signature", "file_type": "document", "node_kind": "heading", "source_file": "specs/005-convert-json-markdown/contracts/cli-contract.md", "source_location": "L5"}, {"id": "$graphify-root$_specs_005_convert_json_markdown_contracts_cli_contract_2_command_line_arguments", "label": "2. Command-Line Arguments", "file_type": "document", "node_kind": "heading", "source_file": "specs/005-convert-json-markdown/contracts/cli-contract.md", "source_location": "L11"}, {"id": "$graphify-root$_specs_005_convert_json_markdown_contracts_cli_contract_3_exit_codes", "label": "3. Exit Codes", "file_type": "document", "node_kind": "heading", "source_file": "specs/005-convert-json-markdown/contracts/cli-contract.md", "source_location": "L18"}, {"id": "$graphify-root$_specs_005_convert_json_markdown_contracts_cli_contract_4_standard_stream_behavior", "label": "4. Standard Stream Behavior", "file_type": "document", "node_kind": "heading", "source_file": "specs/005-convert-json-markdown/contracts/cli-contract.md", "source_location": "L26"}], "edges": [{"source": "$graphify-root$_specs_005_convert_json_markdown_contracts_cli_contract_md", "target": "$graphify-root$_specs_005_convert_json_markdown_contracts_cli_contract_cli_contract_convert_article_to_markdown_py", "relation": "contains", "confidence": "EXTRACTED", "source_file": "specs/005-convert-json-markdown/contracts/cli-contract.md", "source_location": "L1", "weight": 1.0}, {"source": "$graphify-root$_specs_005_convert_json_markdown_contracts_cli_contract_cli_contract_convert_article_to_markdown_py", "target": "$graphify-root$_specs_005_convert_json_markdown_contracts_cli_contract_1_script_signature", "relation": "contains", "confidence": "EXTRACTED", "source_file": "specs/005-convert-json-markdown/contracts/cli-contract.md", "source_location": "L5", "weight": 1.0}, {"source": "$graphify-root$_specs_005_convert_json_markdown_contracts_cli_contract_cli_contract_convert_article_to_markdown_py", "target": "$graphify-root$_specs_005_convert_json_markdown_contracts_cli_contract_2_command_line_arguments", "relation": "contains", "confidence": "EXTRACTED", "source_file": "specs/005-convert-json-markdown/contracts/cli-contract.md", "source_location": "L11", "weight": 1.0}, {"source": "$graphify-root$_specs_005_convert_json_markdown_contracts_cli_contract_cli_contract_convert_article_to_markdown_py", "target": "$graphify-root$_specs_005_convert_json_markdown_contracts_cli_contract_3_exit_codes", "relation": "contains", "confidence": "EXTRACTED", "source_file": "specs/005-convert-json-markdown/contracts/cli-contract.md", "source_location": "L18", "weight": 1.0}, {"source": "$graphify-root$_specs_005_convert_json_markdown_contracts_cli_contract_cli_contract_convert_article_to_markdown_py", "target": "$graphify-root$_specs_005_convert_json_markdown_contracts_cli_contract_4_standard_stream_behavior", "relation": "contains", "confidence": "EXTRACTED", "source_file": "specs/005-convert-json-markdown/contracts/cli-contract.md", "source_location": "L26", "weight": 1.0}], "input_tokens": 0, "output_tokens": 0}
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+20164 -1059
View File
File diff suppressed because it is too large Load Diff
+213 -15
View File
@@ -294,9 +294,9 @@
"semantic_hash": "" "semantic_hash": ""
}, },
"classify.py": { "classify.py": {
"mtime": 1787264412.2457643, "mtime": 1787320064.0177462,
"seen": 1787264519.0539427, "seen": 1787320239.022448,
"ast_hash": "e89fc4b64bd7606fc466d5338108705b", "ast_hash": "3679326c88a59f796f587e0c61c31417",
"semantic_hash": "" "semantic_hash": ""
}, },
"pyproject.toml": { "pyproject.toml": {
@@ -330,15 +330,15 @@
"semantic_hash": "" "semantic_hash": ""
}, },
"src/adapters/llm.py": { "src/adapters/llm.py": {
"mtime": 1787264412.2437606, "mtime": 1787321869.4176295,
"seen": 1787264519.0542035, "seen": 1787322035.8922434,
"ast_hash": "ba2990328f5b25ac6cdfc3b57acc9d39", "ast_hash": "357b505df0d92f76b1d0cd606741d1a5",
"semantic_hash": "" "semantic_hash": ""
}, },
"src/classifier.py": { "src/classifier.py": {
"mtime": 1787264459.052089, "mtime": 1787321309.8981817,
"seen": 1787264519.0542045, "seen": 1787321481.1505442,
"ast_hash": "b8fb440374ecd66bb8bf67c70c19ed1f", "ast_hash": "a4e5dafed12aa4eaf096988b2c6a8ae0",
"semantic_hash": "" "semantic_hash": ""
}, },
"src/language.py": { "src/language.py": {
@@ -420,9 +420,9 @@
"semantic_hash": "" "semantic_hash": ""
}, },
"requirements.txt": { "requirements.txt": {
"mtime": 1787240110.13472, "mtime": 1787317345.9253972,
"seen": 1787240516.5993943, "seen": 1787317712.8213654,
"ast_hash": "f5d99630fa9c93c5fbaa44814f107e70", "ast_hash": "f3f8ea2b8cc995de218d81679ec35231",
"semantic_hash": "" "semantic_hash": ""
}, },
"tests/fixtures/benchmark_24/de/contextual.md": { "tests/fixtures/benchmark_24/de/contextual.md": {
@@ -654,9 +654,9 @@
"semantic_hash": "" "semantic_hash": ""
}, },
"README.md": { "README.md": {
"mtime": 1787264334.4498444, "mtime": 1787321467.0542295,
"seen": 1787264519.0577974, "seen": 1787321481.1561577,
"ast_hash": "196db110cfde06179fb750fc75afb6fe", "ast_hash": "0cde8e800125cbba61a1d7de9d9d2c9e",
"semantic_hash": "" "semantic_hash": ""
}, },
"scripts/extract_article_contents.py": { "scripts/extract_article_contents.py": {
@@ -736,5 +736,203 @@
"seen": 1787240516.6008823, "seen": 1787240516.6008823,
"ast_hash": "d95c4763728496703cd89590288a2bed", "ast_hash": "d95c4763728496703cd89590288a2bed",
"semantic_hash": "" "semantic_hash": ""
},
"scripts/select_article_extractor.py": {
"mtime": 1787274502.4801269,
"seen": 1787310813.8455508,
"ast_hash": "4037eda779d5fc81527d0698a8fc07d6",
"semantic_hash": ""
},
"tests/test_select_article_extractor.py": {
"mtime": 1787274502.4801269,
"seen": 1787310813.8470025,
"ast_hash": "2cd19eb0c5204a8c22b7727e259db885",
"semantic_hash": ""
},
"docs/prd_deterministic_content_selection.md": {
"mtime": 1787264674.2038286,
"seen": 1787272611.6604555,
"ast_hash": "2c421402df8b6469ce60eaa984dce666",
"semantic_hash": ""
},
"specs/004-deterministic-content-selection/checklists/deterministic-selection.md": {
"mtime": 1787272346.8692143,
"seen": 1787272611.6713927,
"ast_hash": "6d9bcda99dbd284ffcbce372102fd868",
"semantic_hash": ""
},
"specs/004-deterministic-content-selection/checklists/requirements.md": {
"mtime": 1787264728.7202728,
"seen": 1787272611.6713977,
"ast_hash": "074efe6ba551e93f7d005c5462805693",
"semantic_hash": ""
},
"specs/004-deterministic-content-selection/contracts/cli-contract.md": {
"mtime": 1787274329.2175446,
"seen": 1787274402.0944166,
"ast_hash": "2cd795eaed6198dc343424bd20673728",
"semantic_hash": ""
},
"specs/004-deterministic-content-selection/contracts/json-schema.md": {
"mtime": 1787271837.7328393,
"seen": 1787272611.6714034,
"ast_hash": "155f0bccb7bcffabcedb6ef86b8003c5",
"semantic_hash": ""
},
"specs/004-deterministic-content-selection/data-model.md": {
"mtime": 1787271825.5970395,
"seen": 1787272611.671406,
"ast_hash": "23ec4a144f4aeeed9fee259b6165bea3",
"semantic_hash": ""
},
"specs/004-deterministic-content-selection/plan.md": {
"mtime": 1787271850.1744206,
"seen": 1787272611.671409,
"ast_hash": "3bebc2a73a9632d42ccca646af592133",
"semantic_hash": ""
},
"specs/004-deterministic-content-selection/quickstart.md": {
"mtime": 1787274502.478128,
"seen": 1787310813.854652,
"ast_hash": "60453533ee75a4f0a894dca634e0c96c",
"semantic_hash": ""
},
"specs/004-deterministic-content-selection/research.md": {
"mtime": 1787274316.8544433,
"seen": 1787274402.0946283,
"ast_hash": "73ac1637d31472e25b0a1709cd137e5c",
"semantic_hash": ""
},
"specs/004-deterministic-content-selection/spec.md": {
"mtime": 1787274311.1564865,
"seen": 1787274402.0946293,
"ast_hash": "b2c994a14871c2e6e1cf4a11db7dbeb5",
"semantic_hash": ""
},
"specs/004-deterministic-content-selection/tasks.md": {
"mtime": 1787272595.8186145,
"seen": 1787272611.6714194,
"ast_hash": "3a2617478f44c6400ed57088d2039f93",
"semantic_hash": ""
},
"scripts/convert_article_to_markdown.py": {
"mtime": 1787320071.2809863,
"seen": 1787320239.0233297,
"ast_hash": "7939a71acd9b264f9d00eab5c652cd36",
"semantic_hash": ""
},
"tests/test_convert_article_to_markdown.py": {
"mtime": 1787320111.811304,
"seen": 1787320239.0292969,
"ast_hash": "5223f35131b8e952e05c44710f3988b2",
"semantic_hash": ""
},
"docs/prd_convert_json_markdown.md": {
"mtime": 1787315400.452784,
"seen": 1787317712.8208659,
"ast_hash": "1fc73d15d4f4d5c1e74f299040b040f2",
"semantic_hash": ""
},
"specs/005-convert-json-markdown/checklists/markdown-conversion.md": {
"mtime": 1787315812.9198053,
"seen": 1787317712.8255877,
"ast_hash": "4cc2161c66aa34dc8540c48322b6eaaa",
"semantic_hash": ""
},
"specs/005-convert-json-markdown/checklists/requirements.md": {
"mtime": 1787315513.450848,
"seen": 1787317712.8255897,
"ast_hash": "a14c648defdfb7c2ad77601249d44a66",
"semantic_hash": ""
},
"specs/005-convert-json-markdown/contracts/cli-contract.md": {
"mtime": 1787315656.382816,
"seen": 1787317712.8255913,
"ast_hash": "bba24a6fcd59b265d11242b4b09d2537",
"semantic_hash": ""
},
"specs/005-convert-json-markdown/contracts/markdown-schema.md": {
"mtime": 1787315662.3060155,
"seen": 1787317712.8255925,
"ast_hash": "4dd6023205dcc49447bf3c78f0419c52",
"semantic_hash": ""
},
"specs/005-convert-json-markdown/data-model.md": {
"mtime": 1787315648.195198,
"seen": 1787317712.8255942,
"ast_hash": "31de71b9fa43fb1b257f76ae2de33044",
"semantic_hash": ""
},
"specs/005-convert-json-markdown/plan.md": {
"mtime": 1787315676.826685,
"seen": 1787317712.8255954,
"ast_hash": "44e3074130dadf889bf3830e6139b91a",
"semantic_hash": ""
},
"specs/005-convert-json-markdown/quickstart.md": {
"mtime": 1787315668.269243,
"seen": 1787317712.8255966,
"ast_hash": "6086387edc574ed31202ff05eaa3fb54",
"semantic_hash": ""
},
"specs/005-convert-json-markdown/research.md": {
"mtime": 1787315639.7204154,
"seen": 1787317712.825598,
"ast_hash": "3ffedbbf7e61245b86119f41a5f8af2d",
"semantic_hash": ""
},
"specs/005-convert-json-markdown/spec.md": {
"mtime": 1787315504.210701,
"seen": 1787317712.825599,
"ast_hash": "9f78ac1c71853dcf4e364c66973dfcbc",
"semantic_hash": ""
},
"specs/005-convert-json-markdown/tasks.md": {
"mtime": 1787317701.507181,
"seen": 1787317712.8256,
"ast_hash": "1287da802887af5e0dd3a6a2685f5fc0",
"semantic_hash": ""
},
"tests/fixtures/markdown_conversion/valid_newspaper4k.md": {
"mtime": 1787317400.9745035,
"seen": 1787317712.8280056,
"ast_hash": "3bfb28bbde97c16635de39bbbe6fbd8b",
"semantic_hash": ""
},
"tests/fixtures/markdown_conversion/valid_readability.md": {
"mtime": 1787317419.4019263,
"seen": 1787317712.8280075,
"ast_hash": "49fdc6bedd7585eda33e2c502337e01a",
"semantic_hash": ""
},
"tests/fixtures/markdown_conversion/valid_trafilatura.md": {
"mtime": 1787317383.5058665,
"seen": 1787317712.8280091,
"ast_hash": "c63c1c39e34e08a239aa8bea3c264756",
"semantic_hash": ""
},
"tests/test_llm_fallback.py": {
"mtime": 1787320797.249565,
"seen": 1787320818.2967606,
"ast_hash": "e5d98de814ceeecd8bd601a7c206e92d",
"semantic_hash": ""
},
"tests/test_e2e_text_analysis_pipeline.py": {
"mtime": 1787321086.751707,
"seen": 1787321205.5156026,
"ast_hash": "3a2d47f2ffcf8371ffdf90bb797b5346",
"semantic_hash": ""
},
"tests/test_classify_exhaustive_suite.py": {
"mtime": 1787321371.2658408,
"seen": 1787321481.15137,
"ast_hash": "08e3c680d8669b2849a19d73b27b869a",
"semantic_hash": ""
},
"tests/README.md": {
"mtime": 1787322185.4865713,
"seen": 1787322203.9119415,
"ast_hash": "ea97839a9079df402573fbe4d0b33faf",
"semantic_hash": ""
} }
} }
+2 -1
View File
@@ -1,5 +1,5 @@
pytest>=7.0.0 pytest>=7.0.0
foxcape>=0.1.1 foxcape>=0.1.2
beautifulsoup4>=4.12.0 beautifulsoup4>=4.12.0
googlenewsdecoder>=0.1.7 googlenewsdecoder>=0.1.7
selectolax>=0.3.27 selectolax>=0.3.27
@@ -7,6 +7,7 @@ trafilatura>=1.8.0
newspaper4k>=0.9.3.1 newspaper4k>=0.9.3.1
readability-lxml>=0.8.1 readability-lxml>=0.8.1
lxml>=4.9.0 lxml>=4.9.0
markdownify>=0.13.0
# Optional Tier 2 / Tier 3 dependencies (not required for POC core execution) # Optional Tier 2 / Tier 3 dependencies (not required for POC core execution)
# sentence-transformers>=2.2.0 # sentence-transformers>=2.2.0
# httpx>=0.24.0 # httpx>=0.24.0
+720
View File
@@ -0,0 +1,720 @@
#!/usr/bin/env python3
"""
Convert Article JSON to Markdown CLI.
Converte o JSON de um único artigo extraído (com selected_extractor) para um documento
Markdown (.md) limpo, padronizado e com seleção determinística de metadados.
"""
from __future__ import annotations
import argparse
import datetime
import email.utils
import html
import json
import re
import sys
import urllib.parse
from pathlib import Path
from typing import Any, Dict, List, Optional, Set
import markdownify
KNOWN_PLACEHOLDERS: Set[str] = {
"null",
"none",
"n/a",
"unknown",
"[no-author]",
"no-author",
}
VALID_EXTRACTORS: Set[str] = {
"trafilatura",
"newspaper4k",
"readability",
}
def normalize_scalar(value: Any) -> Optional[str]:
"""
Decodifica entidades HTML, remove espaços no início/fim, colapsa espaços internos
e descarta placeholders conhecidos.
"""
if not isinstance(value, str):
return None
unescaped = html.unescape(value).strip()
if not unescaped:
return None
collapsed = re.sub(r"\s+", " ", unescaped)
if collapsed.lower() in KNOWN_PLACEHOLDERS:
return None
return collapsed
def normalize_list(value: Any, is_author: bool = False) -> List[str]:
"""
Normaliza listas ou strings separadas por ponto e vírgula, descartando placeholders,
URLs em autores e deduplicando sem diferenciar maiúsculas/minúsculas.
"""
if not value:
return []
raw_items: List[str] = []
if isinstance(value, list):
for item in value:
if isinstance(item, str):
# Se um elemento da lista contiver ponto e vírgula, divide
if ";" in item:
raw_items.extend(item.split(";"))
elif "," in item and not is_author:
# Trafilatura às vezes emite tags separadas por vírgula em string única
raw_items.extend(item.split(","))
else:
raw_items.append(item)
elif isinstance(value, str):
raw_items.extend(value.split(";"))
else:
return []
normalized_items: List[str] = []
seen_lower: Set[str] = set()
for item in raw_items:
norm = normalize_scalar(item)
if not norm:
continue
if is_author:
norm_lower = norm.lower()
if (
norm_lower.startswith("http://")
or norm_lower.startswith("https://")
or norm_lower.startswith("www.")
):
continue
lower_key = norm.lower()
if lower_key not in seen_lower:
seen_lower.add(lower_key)
normalized_items.append(norm)
return normalized_items
def normalize_date(value: Any) -> Optional[str]:
"""
Interpreta datas ISO 8601 e RFC 2822 preservando fuso horário ou YYYY-MM-DD para datas puras.
"""
if not isinstance(value, str):
return None
s = value.strip()
if not s or s.lower() in KNOWN_PLACEHOLDERS:
return None
# Se for apenas data YYYY-MM-DD
if re.match(r"^\d{4}-\d{2}-\d{2}$", s):
return s
# Tenta ISO 8601
try:
dt = datetime.datetime.fromisoformat(s)
return dt.isoformat()
except (ValueError, TypeError):
pass
# Tenta RFC 2822
try:
dt = email.utils.parsedate_to_datetime(s)
return dt.isoformat()
except (ValueError, TypeError):
pass
return None
def validate_url(value: Any) -> Optional[str]:
"""Valida se a URL é absoluta com protocolo http ou https e hostname não vazio."""
norm = normalize_scalar(value)
if not norm:
return None
try:
parsed = urllib.parse.urlparse(norm)
if parsed.scheme.lower() in ("http", "https") and parsed.netloc:
return norm
except Exception:
pass
return None
def convert_html_to_markdown(html_content: Optional[str]) -> str:
"""Converte HTML para Markdown usando títulos ATX, removendo scripts e estilos."""
if not html_content or not isinstance(html_content, str) or not html_content.strip():
return ""
# Remove blocos completos de <script> e <style> incluindo conteúdo
sanitized_html = re.sub(
r"<(script|style)[^>]*>.*?</\1>",
"",
html_content,
flags=re.DOTALL | re.IGNORECASE,
)
md = markdownify.markdownify(
sanitized_html,
heading_style=markdownify.ATX,
)
return md.strip()
def resolve_article_body(article: Dict[str, Any]) -> str:
"""
Obtém o corpo do artigo exclusivamente do selected_extractor com fallback interno
(markdown/html -> text). Falha se o extrator selecionado não contiver corpo.
"""
selected = article.get("selected_extractor")
if not selected or selected not in VALID_EXTRACTORS:
raise ValueError(
f"selected_extractor inválido ou ausente: '{selected}'. "
f"Valores permitidos: {', '.join(sorted(VALID_EXTRACTORS))}"
)
extractor_data = article.get(selected)
if not isinstance(extractor_data, dict):
raise ValueError(f"Objeto do extrator selecionado '{selected}' ausente na entrada.")
body: Optional[str] = None
if selected == "trafilatura":
primary = extractor_data.get("markdown")
if isinstance(primary, str) and primary.strip():
body = primary.strip()
else:
fallback = extractor_data.get("text")
if isinstance(fallback, str) and fallback.strip():
body = fallback.strip()
elif selected == "newspaper4k":
primary = extractor_data.get("article_html")
if isinstance(primary, str) and primary.strip():
converted = convert_html_to_markdown(primary)
if converted:
body = converted
if not body:
fallback = extractor_data.get("text")
if isinstance(fallback, str) and fallback.strip():
body = fallback.strip()
elif selected == "readability":
primary = extractor_data.get("cleaned_html")
if isinstance(primary, str) and primary.strip():
converted = convert_html_to_markdown(primary)
if converted:
body = converted
if not body:
fallback = extractor_data.get("cleaned_text")
if isinstance(fallback, str) and fallback.strip():
body = fallback.strip()
if not body or not body.strip():
raise ValueError(
f"Corpo do extrator selecionado '{selected}' está vazio ou indisponível. "
"Proibido fallback para outro extrator."
)
return body.strip()
def _get_dict(data: Dict[str, Any], key: str) -> Dict[str, Any]:
"""Retorna o dicionário associado à chave ou um dicionário vazio caso não seja dict."""
val = data.get(key)
return val if isinstance(val, dict) else {}
def resolve_article_metadata(article: Dict[str, Any]) -> Dict[str, Any]:
"""
Resolve todos os metadados do artigo seguindo a matriz estrita de prioridades do PRD.
"""
selected = str(article.get("selected_extractor", ""))
input_meta = _get_dict(article, "input_meta")
trafilatura = _get_dict(article, "trafilatura")
newspaper = _get_dict(article, "newspaper4k")
readability = _get_dict(article, "readability")
sel_data = _get_dict(article, selected)
# 1. TÍTULO
title_candidates: List[Any] = []
if selected in ("trafilatura", "newspaper4k", "readability"):
title_candidates.append(sel_data.get("title"))
title_candidates.extend(
[
input_meta.get("titulo"),
article.get("page_title"),
newspaper.get("title"),
trafilatura.get("title"),
readability.get("title"),
]
)
resolved_title: Optional[str] = None
for cand in title_candidates:
val = normalize_scalar(cand)
if val:
resolved_title = val
break
if not resolved_title:
raise ValueError("Título do artigo não pôde ser resolvido a partir de nenhuma fonte.")
# 2. URL ORIGINAL
canonical_sel = None
if selected == "trafilatura":
canonical_sel = sel_data.get("canonical_url")
elif selected == "newspaper4k":
canonical_sel = sel_data.get("canonical_link")
url_candidates = [
input_meta.get("url"),
article.get("crawled_url"),
canonical_sel,
trafilatura.get("canonical_url"),
newspaper.get("canonical_link"),
]
resolved_url: Optional[str] = None
for cand in url_candidates:
val = validate_url(cand)
if val:
resolved_url = val
break
if not resolved_url:
raise ValueError(
"URL original válida (http/https) não pôde ser resolvida a partir de nenhuma fonte."
)
# 3. SUBTÍTULO / DESCRIÇÃO
desc_sel = None
if selected == "trafilatura":
desc_sel = sel_data.get("description")
elif selected == "newspaper4k":
desc_sel = sel_data.get("meta_description")
desc_candidates = [
desc_sel,
trafilatura.get("description"),
newspaper.get("meta_description"),
input_meta.get("subtitulo"),
]
resolved_subtitle: Optional[str] = None
for cand in desc_candidates:
val = normalize_scalar(cand)
if val:
# Omitir quando for igual ao título após normalização
if val.lower() != resolved_title.lower():
resolved_subtitle = val
break
# 4. AUTORES
author_sel = None
if selected == "trafilatura":
author_sel = sel_data.get("author")
elif selected == "newspaper4k":
author_sel = sel_data.get("authors")
elif selected == "readability":
author_sel = sel_data.get("author")
author_candidates = [
author_sel,
newspaper.get("authors"),
trafilatura.get("author"),
readability.get("author"),
]
resolved_authors: List[str] = []
for cand in author_candidates:
lst = normalize_list(cand, is_author=True)
if lst:
resolved_authors = lst
break
# 5. DATA DE PUBLICAÇÃO
date_sel = None
if selected == "trafilatura":
date_sel = sel_data.get("date")
elif selected == "newspaper4k":
date_sel = sel_data.get("publish_date")
date_candidates = [
date_sel,
newspaper.get("publish_date"),
trafilatura.get("date"),
input_meta.get("quando_publicado"),
]
resolved_date: Optional[str] = None
for cand in date_candidates:
val = normalize_date(cand)
if val:
resolved_date = val
break
# 6. SITE
site_sel = None
if selected == "trafilatura":
site_sel = sel_data.get("sitename")
elif selected == "newspaper4k":
site_sel = sel_data.get("meta_site_name")
url_hostname = urllib.parse.urlparse(resolved_url).netloc if resolved_url else None
site_candidates = [
site_sel,
trafilatura.get("sitename"),
newspaper.get("meta_site_name"),
trafilatura.get("hostname"),
url_hostname,
]
resolved_site: Optional[str] = None
for cand in site_candidates:
val = normalize_scalar(cand)
if val:
resolved_site = val
break
# 7. CATEGORIAS
cat_sel = sel_data.get("categories") if selected == "trafilatura" else None
cat_candidates = [
cat_sel,
trafilatura.get("categories"),
]
resolved_categories: List[str] = []
for cand in cat_candidates:
lst = normalize_list(cand)
if lst:
resolved_categories = lst
break
# 8. TAGS
tag_sel = None
if selected == "trafilatura":
tag_sel = sel_data.get("tags")
elif selected == "newspaper4k":
tag_sel = sel_data.get("tags")
tag_candidates = [
tag_sel,
trafilatura.get("tags"),
newspaper.get("tags"),
newspaper.get("meta_keywords"),
]
resolved_tags: List[str] = []
for cand in tag_candidates:
lst = normalize_list(cand)
if lst:
resolved_tags = lst
break
# 9. PALAVRAS-CHAVE
kw_candidates = [
newspaper.get("keywords"),
newspaper.get("meta_keywords"),
]
resolved_keywords: List[str] = []
for cand in kw_candidates:
lst = normalize_list(cand)
if lst:
resolved_keywords = lst
break
# 10. IDIOMA
lang_sel = None
if selected == "trafilatura":
lang_sel = sel_data.get("language")
elif selected == "newspaper4k":
lang_sel = sel_data.get("meta_lang")
lang_candidates = [
lang_sel,
trafilatura.get("language"),
newspaper.get("meta_lang"),
]
resolved_language: Optional[str] = None
for cand in lang_candidates:
val = normalize_scalar(cand)
if val:
resolved_language = val
break
# 11. IMAGEM PRINCIPAL
img_sel = None
if selected == "trafilatura":
img_sel = sel_data.get("image")
elif selected == "newspaper4k":
img_sel = sel_data.get("top_image")
img_candidates = [
img_sel,
newspaper.get("top_image"),
trafilatura.get("image"),
]
resolved_top_image: Optional[str] = None
for cand in img_candidates:
val = validate_url(cand)
if val:
resolved_top_image = val
break
return {
"title": resolved_title,
"original_url": resolved_url,
"subtitle": resolved_subtitle,
"authors": resolved_authors,
"publish_date": resolved_date,
"site_name": resolved_site,
"categories": resolved_categories,
"tags": resolved_tags,
"keywords": resolved_keywords,
"language": resolved_language,
"top_image": resolved_top_image,
}
def remove_duplicate_initial_h1(body: str, resolved_title: str) -> str:
"""
Remove o primeiro título H1 do corpo somente quando ele for igual ao título resolvido
(comparação case-insensitive após decodificação HTML e colapso de espaços).
"""
if not body:
return ""
lines = body.splitlines()
first_h1_idx: Optional[int] = None
for i, line in enumerate(lines):
stripped = line.strip()
if not stripped:
continue
if stripped.startswith("# "):
h1_text = stripped[2:].strip()
norm_h1 = normalize_scalar(h1_text)
norm_title = normalize_scalar(resolved_title)
if norm_h1 and norm_title and norm_h1.lower() == norm_title.lower():
first_h1_idx = i
break
else:
# Encontrou outro conteúdo antes de qualquer H1
break
if first_h1_idx is not None:
lines.pop(first_h1_idx)
# Remove linhas em branco residuais no início
while lines and not lines[0].strip():
lines.pop(0)
return "\n".join(lines)
def clean_body_images(body: str) -> str:
"""
Preserva imagens com URL absoluta http/https, remove relativas/data:/vazias e
deduplica repetições exatas da mesma URL de imagem.
"""
if not body:
return ""
seen_images: Set[str] = set()
def replace_image(match: re.Match) -> str:
alt_text = match.group(1)
raw_url = match.group(2).strip()
# Extrai URL se tiver atributos extras como '<url 960w>' ou srcset
clean_url = raw_url.split()[0].strip() if raw_url else ""
valid = validate_url(clean_url)
if not valid:
return ""
if valid in seen_images:
return ""
seen_images.add(valid)
return f"![{alt_text}]({valid})"
# Expressão regular para imagem Markdown ![alt](url)
pattern = r"!\[(.*?)\]\((.*?)\)"
cleaned = re.sub(pattern, replace_image, body)
return cleaned
def assemble_markdown_document(meta: Dict[str, Any], body: str) -> str:
"""
Monta a estrutura final do documento Markdown respeitando a ordem estrita do PRD:
# Título
Subtítulo (se houver)
Bloco de metadados
![Imagem principal](url) (se houver)
---
Conteúdo do corpo
"""
sections: List[str] = []
# 1. Título
sections.append(f"# {meta['title']}")
# 2. Subtítulo (quando disponível e diferente do título)
if meta.get("subtitle"):
sections.append(meta["subtitle"])
# 3. Metadados
meta_lines: List[str] = []
if meta.get("authors"):
meta_lines.append(f"**Autor:** {', '.join(meta['authors'])}")
if meta.get("publish_date"):
meta_lines.append(f"**Publicado em:** {meta['publish_date']}")
if meta.get("site_name"):
meta_lines.append(f"**Site:** {meta['site_name']}")
if meta.get("categories"):
meta_lines.append(f"**Categoria:** {', '.join(meta['categories'])}")
if meta.get("tags"):
meta_lines.append(f"**Tags:** {', '.join(meta['tags'])}")
if meta.get("keywords"):
meta_lines.append(f"**Palavras-chave:** {', '.join(meta['keywords'])}")
if meta.get("language"):
meta_lines.append(f"**Idioma:** {meta['language']}")
if meta.get("original_url"):
meta_lines.append(f"**Fonte original:** [{meta['original_url']}]({meta['original_url']})")
if meta_lines:
sections.append("\n".join(meta_lines))
# 4. Imagem principal
if meta.get("top_image"):
sections.append(f"![Imagem principal]({meta['top_image']})")
# 5. Separador
sections.append("---")
# 6. Corpo
sections.append(body.strip())
# Junção com 2 quebras de linha
raw_doc = "\n\n".join(sections)
# Formatação final:
# 1. Quebras LF
raw_doc = raw_doc.replace("\r\n", "\n").replace("\r", "\n")
# 2. Remover espaços no fim de linha
lines = [line.rstrip() for line in raw_doc.split("\n")]
formatted_doc = "\n".join(lines)
# 3. Limitar linhas em branco consecutivas a no máximo 2 (\n\n\n -> \n\n)
formatted_doc = re.sub(r"\n{3,}", "\n\n", formatted_doc)
# 4. Terminar com exatamente 1 quebra de linha
formatted_doc = formatted_doc.strip() + "\n"
return formatted_doc
def convert_article(input_path: Path, output_path: Optional[Path] = None) -> Path:
"""
Executa a leitura do JSON, validação, conversão e escrita atômica do arquivo Markdown.
"""
if not input_path.exists() or not input_path.is_file():
raise FileNotFoundError(f"Arquivo de entrada não encontrado ou ilegível: '{input_path}'")
try:
content = input_path.read_text(encoding="utf-8")
data = json.loads(content)
except UnicodeDecodeError as e:
raise ValueError(f"Arquivo '{input_path}' não está codificado em UTF-8 válido: {e}")
except json.JSONDecodeError as e:
raise ValueError(f"Entrada não é um JSON válido: {e}")
if not isinstance(data, dict):
raise ValueError(f"A raiz do JSON deve ser um objeto, mas recebeu '{type(data).__name__}'.")
if "articles" in data:
raise ValueError(
"O arquivo JSON contém uma coleção 'articles'. O CLI aceita apenas um único artigo por execução."
)
# Resolução de corpo e metadados
body_raw = resolve_article_body(data)
metadata = resolve_article_metadata(data)
# Limpezas no corpo
body_no_dup_h1 = remove_duplicate_initial_h1(body_raw, metadata["title"])
body_clean_images = clean_body_images(body_no_dup_h1)
# Montagem final
final_markdown = assemble_markdown_document(metadata, body_clean_images)
# Definição do caminho de saída
if output_path is None:
target_path = input_path.with_suffix(".md")
else:
target_path = output_path
# Garantir que o diretório de destino exista
target_path.parent.mkdir(parents=True, exist_ok=True)
# Gravação atômica: arquivo temporário no mesmo diretório + replace
temp_file = target_path.parent / f".{target_path.name}.tmp"
try:
temp_file.write_text(final_markdown, encoding="utf-8", newline="\n")
temp_file.replace(target_path)
except Exception as e:
if temp_file.exists():
try:
temp_file.unlink()
except OSError:
pass
raise IOError(f"Falha na gravação do arquivo de saída '{target_path}': {e}")
return target_path
def parse_arguments(args: Optional[List[str]] = None) -> argparse.Namespace:
"""Configura o parser de argumentos do CLI."""
parser = argparse.ArgumentParser(
description="Converte JSON de artigo selecionado para Markdown estruturado e determinístico."
)
parser.add_argument(
"-i",
"--input",
required=True,
type=Path,
help="Caminho para o arquivo JSON contendo exatamente um único artigo.",
)
parser.add_argument(
"-o",
"--output",
required=False,
type=Path,
default=None,
help="Caminho do arquivo Markdown de destino (padrão: <input_stem>.md).",
)
return parser.parse_args(args)
def main() -> int:
"""Ponto de entrada do CLI."""
try:
args = parse_arguments()
except SystemExit as e:
return e.code if isinstance(e.code, int) else 2
try:
out_file = convert_article(args.input, args.output)
sys.stderr.write(f"[INFO] Artigo convertido com sucesso: '{out_file}'\n")
return 0
except (FileNotFoundError, ValueError, IOError) as e:
sys.stderr.write(f"[ERRO] {e}\n")
return 1
except Exception as e:
sys.stderr.write(f"[ERRO INESPERADO] {type(e).__name__}: {e}\n")
return 1
if __name__ == "__main__":
sys.exit(main())
+603
View File
@@ -0,0 +1,603 @@
#!/usr/bin/env python3
"""
Seletor Determinístico de Extrator de Conteúdo de Artigos.
Compara deterministicamente as saídas de Trafilatura, Newspaper4k e Readability
a partir de um arquivo JSON consolidado de extrações, calculando métricas de consenso
(shingles de 5-tokens, cobertura, suporte, F1-score) e aplicando regras rígidas de
desempate técnico e hierárquico, enriquecendo o JSON exclusivamente com a chave
`selected_extractor` de forma não-destrutiva e atômica.
"""
from __future__ import annotations
import argparse
import html
import json
import os
import re
import sys
import tempfile
import unicodedata
from collections import Counter
from dataclasses import dataclass, field
from enum import Enum
from pathlib import Path
from typing import Any
# ==============================================================================
# Modelos de Dados e Enumerações
# ==============================================================================
class ExtractorName(str, Enum):
"""Nomes canônicos e catálogo fechado dos motores de extração."""
TRAFILATURA = "trafilatura"
NEWSPAPER4K = "newspaper4k"
READABILITY = "readability"
class CandidateStatus(str, Enum):
"""Estado de viabilidade do candidato para formação do conjunto ativo."""
USABLE = "usable"
DEGRADED = "degraded"
UNAVAILABLE = "unavailable"
# Ordem estrita de prioridade de desempate final (PRD §7.1 item 4, §7.5 item 6, §7.6 item 4)
FALLBACK_PRIORITY: list[ExtractorName] = [
ExtractorName.NEWSPAPER4K,
ExtractorName.READABILITY,
ExtractorName.TRAFILATURA,
]
# Margem de empate técnico (PRD §7.5 item 3)
TECHNICAL_TIE_THRESHOLD: float = 0.03
FLOAT_EPSILON: float = 1e-9
@dataclass
class ExtractorCandidate:
"""Representação e métricas de um candidato a extrator em um artigo."""
name: ExtractorName
raw_text: str | None = None
error: str | None = None
status: CandidateStatus = CandidateStatus.UNAVAILABLE
tokens: list[str] = field(default_factory=list)
shingles: set[tuple[str, ...]] = field(default_factory=set)
shingle_count: int = 0
coverage: float = 0.0
support: float = 0.0
score: float = 0.0
@dataclass
class ArticleSelectionResult:
"""Resultado detalhado da seleção para um artigo individual."""
article_index: int
selected_extractor: ExtractorName
selection_reason: str
active_candidates_count: int
consensus_shingles_count: int
candidates: dict[ExtractorName, ExtractorCandidate] = field(default_factory=dict)
@dataclass
class BatchProcessingResult:
"""Resultado consolidado do processamento em lote."""
total_articles: int
processed_count: int
selection_distribution: dict[str, int]
input_file: str
output_file: str
selections: list[ArticleSelectionResult] = field(default_factory=list)
# ==============================================================================
# Pipeline de Normalização Textual e Shingles
# ==============================================================================
def normalize_text(text: Any) -> list[str]:
"""
Executa a normalização determinística para comparação:
1. Decodificar entidades HTML.
2. Remover marcação HTML e Markdown, preservando o texto visível.
3. Em links, preservar o texto e remover o endereço.
4. Aplicar normalização Unicode NFKC.
5. Converter o texto para minúsculas.
6. Substituir toda sequência de espaços, tabulações ou quebras de linha por um único espaço.
7. Tokenizar mantendo letras e números Unicode.
8. Desconsiderar pontuação.
"""
if not text or not isinstance(text, str):
return []
# 1. Decodificar entidades HTML
s = html.unescape(text)
# 2. Remover imagens Markdown ![alt](url) -> '' (marcação de mídia não é texto visível)
s = re.sub(r"!\s*\[[^\]]*\]\([^)]*\)", " ", s)
# 3. Em links Markdown [texto](url), preservar texto âncora e remover endereço
s = re.sub(r"\[([^\]]+)\]\([^)]+\)", r" \1 ", s)
# 2. Remover tags HTML mantendo espaço entre palavras
s = re.sub(r"<[^>]+>", " ", s)
# 4. Normalização Unicode NFKC
s = unicodedata.normalize("NFKC", s)
# 5. Minúsculas
s = s.lower()
# 6. Colapso de múltiplos espaços em branco
s = re.sub(r"\s+", " ", s).strip()
# 7 & 8. Tokenizar mantendo letras e números Unicode, ignorando pontuações
tokens = re.findall(r"[\w]+", s, flags=re.UNICODE)
return tokens
def generate_shingles(tokens: list[str], window_size: int = 5) -> set[tuple[str, ...]]:
"""
Gera conjunto de shingles ordenados de tamanho window_size (padrão 5).
- Se len(tokens) >= window_size: todas as janelas consecutivas de 5 tokens.
- Se 1 <= len(tokens) < window_size: sequência completa como um único shingle.
- Se tokens vazio: conjunto vazio.
"""
n = len(tokens)
if n == 0:
return set()
if n < window_size:
return {tuple(tokens)}
return {tuple(tokens[i : i + window_size]) for i in range(n - window_size + 1)}
# ==============================================================================
# Classificação de Candidatos e Formação do Conjunto Ativo
# ==============================================================================
def extract_candidate_data(article_dict: dict[str, Any], name: ExtractorName) -> ExtractorCandidate:
"""Extrai campo de texto, erro e calcula tokens/shingles para um motor."""
lib_data = article_dict.get(name.value)
if not isinstance(lib_data, dict):
return ExtractorCandidate(name=name, status=CandidateStatus.UNAVAILABLE)
# Campo usado na comparação (PRD §5.2)
if name == ExtractorName.TRAFILATURA:
raw_text = lib_data.get("text")
elif name == ExtractorName.NEWSPAPER4K:
raw_text = lib_data.get("text")
elif name == ExtractorName.READABILITY:
raw_text = lib_data.get("cleaned_text")
else:
raw_text = None
raw_error = lib_data.get("error")
# Tratar erro vazio/nulo
error_val = str(raw_error) if raw_error is not None and str(raw_error).strip() else None
# Normalizar texto para obter tokens
tokens = normalize_text(raw_text)
shingles = generate_shingles(tokens)
shingle_count = len(shingles)
# Determinar status do candidato (PRD §5.3)
if not tokens or shingle_count == 0:
status = CandidateStatus.UNAVAILABLE
elif error_val is None:
status = CandidateStatus.USABLE
else:
status = CandidateStatus.DEGRADED
return ExtractorCandidate(
name=name,
raw_text=raw_text if isinstance(raw_text, str) else None,
error=error_val,
status=status,
tokens=tokens,
shingles=shingles,
shingle_count=shingle_count,
)
def form_active_set(
candidates: dict[ExtractorName, ExtractorCandidate],
) -> list[ExtractorCandidate]:
"""
Forma o conjunto ativo de candidatos conforme PRD §7.1:
1. Se existir pelo menos um utilizável, considerar somente os utilizáveis.
2. Se não existir utilizável, considerar os candidatos degradados.
3. Se não existir utilizável nem degradado, retorna lista vazia.
"""
usables = [c for c in candidates.values() if c.status == CandidateStatus.USABLE]
if usables:
return usables
degradeds = [c for c in candidates.values() if c.status == CandidateStatus.DEGRADED]
if degradeds:
return degradeds
return []
# ==============================================================================
# Cálculo de Consenso e Métricas F1
# ==============================================================================
def calculate_consensus_metrics(
active_candidates: list[ExtractorCandidate],
) -> set[tuple[str, ...]]:
"""
Constrói o conjunto de consenso (shingles presentes em >= 2 candidatos ativos)
e calcula Cobertura, Suporte e F1-score para cada candidato ativo (PRD §7.4).
"""
if len(active_candidates) < 2:
for c in active_candidates:
c.coverage = 1.0 if c.shingle_count > 0 else 0.0
c.support = 1.0 if c.shingle_count > 0 else 0.0
c.score = 1.0 if c.shingle_count > 0 else 0.0
return set()
# Contar frequência de cada shingle entre os candidatos ativos
shingle_counts: Counter[tuple[str, ...]] = Counter()
for c in active_candidates:
for s in c.shingles:
shingle_counts[s] += 1
consensus_shingles = {s for s, cnt in shingle_counts.items() if cnt >= 2}
total_consensus = len(consensus_shingles)
for c in active_candidates:
if total_consensus == 0 or c.shingle_count == 0:
c.coverage = 0.0
c.support = 0.0
c.score = 0.0
continue
common_shingles = len(c.shingles & consensus_shingles)
c.coverage = common_shingles / total_consensus
c.support = common_shingles / c.shingle_count
denominator = c.coverage + c.support
if denominator > 0:
c.score = (2.0 * c.coverage * c.support) / denominator
else:
c.score = 0.0
return consensus_shingles
# ==============================================================================
# Algoritmos de Seleção e Desempate
# ==============================================================================
def break_priority_tie(candidates: list[ExtractorCandidate]) -> ExtractorCandidate:
"""Aplica a prioridade final estrita: newspaper4k > readability > trafilatura."""
for priority_name in FALLBACK_PRIORITY:
for c in candidates:
if c.name == priority_name:
return c
return candidates[0]
def select_with_consensus(
active_candidates: list[ExtractorCandidate], consensus_shingles: set[tuple[str, ...]]
) -> tuple[ExtractorName, str]:
"""
Seleciona o melhor candidato quando existe consenso (PRD §7.5):
1. Ordenar por score decrescente.
2. Identificar candidatos no empate técnico (diferença para maior score <= 0.03).
3. Se houver 1 candidato no empate técnico, selecioná-lo.
4. Se houver empate técnico, selecionar o candidato com menor quantidade de shingles.
5. Se empatar na quantidade de shingles, aplicar prioridade final newspaper4k > readability > trafilatura.
"""
# Ordenar por score decrescente
sorted_by_score = sorted(active_candidates, key=lambda c: c.score, reverse=True)
max_score = sorted_by_score[0].score
# Grupo de empate técnico (score >= max_score - 0.03 com tolerância de precisão float)
technical_tie_pool = [
c
for c in sorted_by_score
if (max_score - c.score) <= (TECHNICAL_TIE_THRESHOLD + FLOAT_EPSILON)
]
if len(technical_tie_pool) == 1:
return technical_tie_pool[0].name, "highest_score"
# Selecionar o candidato com menor quantidade de shingles no grupo de empate
min_shingles = min(c.shingle_count for c in technical_tie_pool)
shingle_tie_pool = [c for c in technical_tie_pool if c.shingle_count == min_shingles]
if len(shingle_tie_pool) == 1:
return shingle_tie_pool[0].name, "technical_tie_smallest_shingles"
# Desempate final de prioridade
winner = break_priority_tie(shingle_tie_pool)
return winner.name, "technical_tie_priority_fallback"
def select_without_consensus(
active_candidates: list[ExtractorCandidate],
) -> tuple[ExtractorName, str]:
"""
Seleciona o candidato quando NÃO existe consenso (PRD §7.6):
- Com 3 candidatos ativos: selecionar o candidato com a quantidade mediana de shingles.
- Com 2 candidatos ativos: selecionar o candidato com a maior quantidade de shingles.
- Com 1 candidato ativo: selecionar o único candidato.
- Em empate de quantidade: aplicar prioridade newspaper4k > readability > trafilatura.
- Sem candidato ativo: selecionar newspaper4k.
"""
k = len(active_candidates)
if k == 0:
return ExtractorName.NEWSPAPER4K, "fallback_all_unavailable"
if k == 1:
return active_candidates[0].name, "single_active_candidate"
if k == 2:
c1, c2 = active_candidates[0], active_candidates[1]
if c1.shingle_count > c2.shingle_count:
return c1.name, "no_consensus_max_shingles"
elif c2.shingle_count > c1.shingle_count:
return c2.name, "no_consensus_max_shingles"
else:
winner = break_priority_tie([c1, c2])
return winner.name, "no_consensus_tie_priority_fallback"
if k == 3:
# 3 candidatos ativos -> quantidade mediana de shingles
# Ordenar por shingle_count crescente
sorted_by_shingles = sorted(active_candidates, key=lambda c: c.shingle_count)
s0, s1, s2 = (
sorted_by_shingles[0].shingle_count,
sorted_by_shingles[1].shingle_count,
sorted_by_shingles[2].shingle_count,
)
# Se todos tiverem contagens distintas (ex: 10, 20, 30), a mediana é o elemento do meio (20)
if s0 < s1 < s2:
return sorted_by_shingles[1].name, "no_consensus_median_shingles"
# Se houver empate na mediana (ex: [10, 20, 20] ou [20, 20, 30] ou [20, 20, 20])
# Os candidatos cujo shingle_count é igual ao valor mediano (s1) entram no pool de desempate
median_value = s1
median_candidates = [c for c in active_candidates if c.shingle_count == median_value]
if len(median_candidates) == 1:
return median_candidates[0].name, "no_consensus_median_shingles"
else:
winner = break_priority_tie(median_candidates)
return winner.name, "no_consensus_median_priority_fallback"
# Fallback genérico para listas maiores (se houver)
winner = break_priority_tie(active_candidates)
return winner.name, "priority_fallback"
def select_article_extractor(
article_dict: dict[str, Any], article_index: int = 0
) -> ArticleSelectionResult:
"""
Orquestrador determinístico completo para um único artigo.
Classifica candidatos, forma conjunto ativo, calcula métricas e aplica regras de decisão.
"""
candidates: dict[ExtractorName, ExtractorCandidate] = {
name: extract_candidate_data(article_dict, name) for name in ExtractorName
}
active_set = form_active_set(candidates)
if not active_set:
# Nenhum candidato utilizável nem degradado -> Fallback compulsório para newspaper4k (PRD §7.1 item 4)
return ArticleSelectionResult(
article_index=article_index,
selected_extractor=ExtractorName.NEWSPAPER4K,
selection_reason="fallback_all_unavailable",
active_candidates_count=0,
consensus_shingles_count=0,
candidates=candidates,
)
if len(active_set) == 1:
# Apenas 1 candidato ativo -> selecioná-lo imediatamente (PRD §7.1 item 5)
winner = active_set[0]
return ArticleSelectionResult(
article_index=article_index,
selected_extractor=winner.name,
selection_reason="single_usable_candidate"
if winner.status == CandidateStatus.USABLE
else "single_degraded_candidate",
active_candidates_count=1,
consensus_shingles_count=0,
candidates=candidates,
)
# Calcular consenso e métricas F1
consensus_shingles = calculate_consensus_metrics(active_set)
if consensus_shingles:
winner_name, reason = select_with_consensus(active_set, consensus_shingles)
else:
winner_name, reason = select_without_consensus(active_set)
return ArticleSelectionResult(
article_index=article_index,
selected_extractor=winner_name,
selection_reason=reason,
active_candidates_count=len(active_set),
consensus_shingles_count=len(consensus_shingles),
candidates=candidates,
)
# ==============================================================================
# Processamento em Lote e I/O Atômico
# ==============================================================================
def atomic_save_json(data: Any, target_path: Path, indent: int = 2) -> None:
"""Salva dados em JSON de forma atômica utilizando arquivo temporário e rename."""
target_path = Path(target_path).resolve()
target_path.parent.mkdir(parents=True, exist_ok=True)
temp_fd, temp_file_path = tempfile.mkstemp(
dir=target_path.parent, prefix=f".{target_path.name}.tmp_", text=True
)
try:
with open(temp_fd, "w", encoding="utf-8") as f:
if indent > 0:
json.dump(data, f, ensure_ascii=False, indent=indent)
else:
json.dump(data, f, ensure_ascii=False, separators=(",", ":"))
f.write("\n")
os.replace(temp_file_path, target_path)
except Exception:
if os.path.exists(temp_file_path):
os.remove(temp_file_path)
raise
def process_batch(
input_path: Path | str,
output_path: Path | str | None = None,
indent: int = 2,
verbose: bool = False,
) -> BatchProcessingResult:
"""
Lê o JSON de entrada, valida a estrutura, processa todos os artigos e grava o arquivo de saída.
Preserva 100% dos dados originais e a ordem dos artigos.
"""
in_file = Path(input_path).resolve()
if not in_file.is_file():
raise FileNotFoundError(f"Arquivo de entrada não encontrado: {in_file}")
try:
with open(in_file, "r", encoding="utf-8") as f:
data = json.load(f)
except json.JSONDecodeError as exc:
raise ValueError(f"JSON inválido em '{in_file}': {exc}") from exc
if not isinstance(data, dict):
raise ValueError("A raiz do JSON de entrada deve ser um objeto.")
articles = data.get("articles")
if articles is None or not isinstance(articles, list):
raise ValueError("A chave 'articles' é obrigatória e deve ser uma lista.")
if output_path is None:
# Padrão: <nome_original_sem_extensao>_selected.json
out_file = in_file.parent / f"{in_file.stem}_selected.json"
else:
out_file = Path(output_path).resolve()
selections: list[ArticleSelectionResult] = []
distribution: Counter[str] = Counter()
for idx, article in enumerate(articles):
if not isinstance(article, dict):
# Tratar artigo malformado
article = {}
articles[idx] = article
res = select_article_extractor(article, article_index=idx)
selections.append(res)
distribution[res.selected_extractor.value] += 1
# Enriquecer ou recalcular chave no artigo (PRD §6.2 e §6.3)
article["selected_extractor"] = res.selected_extractor.value
if verbose:
sys.stderr.write(
f"[Artigo #{idx + 1:03d}] Extrator: {res.selected_extractor.value:<12} | "
f"Motivo: {res.selection_reason:<32} | Ativos: {res.active_candidates_count} | "
f"Consenso: {res.consensus_shingles_count}\n"
)
# Gravar arquivo de saída atomicamente
atomic_save_json(data, out_file, indent=indent)
return BatchProcessingResult(
total_articles=len(articles),
processed_count=len(selections),
selection_distribution=dict(distribution),
input_file=str(in_file),
output_file=str(out_file),
selections=selections,
)
# ==============================================================================
# Interface CLI
# ==============================================================================
def parse_args(argv: list[str] | None = None) -> argparse.Namespace:
parser = argparse.ArgumentParser(
description="Seletor determinístico da melhor extração de conteúdo (Trafilatura / Newspaper4k / Readability)."
)
parser.add_argument(
"input_file", type=Path, help="Caminho do arquivo JSON consolidado de extrações."
)
parser.add_argument(
"-o",
"--output",
type=Path,
default=None,
help="Caminho do arquivo JSON de saída (padrão: <nome>_selected.json).",
)
parser.add_argument(
"--indent", type=int, default=2, help="Indentação do arquivo JSON de saída (padrão: 2)."
)
parser.add_argument(
"-v",
"--verbose",
action="store_true",
help="Exibe detalhes da seleção e pontuações no stderr.",
)
return parser.parse_args(argv)
def main(argv: list[str] | None = None) -> int:
args = parse_args(argv)
try:
result = process_batch(
input_path=args.input_file,
output_path=args.output,
indent=args.indent,
verbose=args.verbose,
)
output_summary = {
"status": "success",
"input_file": result.input_file,
"output_file": result.output_file,
"total_articles": result.total_articles,
"processed_count": result.processed_count,
"distribution": result.selection_distribution,
}
sys.stdout.write(json.dumps(output_summary, indent=2, ensure_ascii=False) + "\n")
return 0
except FileNotFoundError as e:
sys.stderr.write(f"ERRO DE ARQUIVO: {e}\n")
return 1
except ValueError as e:
sys.stderr.write(f"ERRO DE VALIDAÇÃO: {e}\n")
return 2
except Exception as e:
sys.stderr.write(f"ERRO INESPERADO: {e}\n")
return 1
if __name__ == "__main__":
sys.exit(main())
@@ -0,0 +1,54 @@
# Deterministic Content Selection Checklist: End-to-End Requirements Quality
**Purpose**: Validate the completeness, clarity, consistency, and measurability of requirements for the deterministic extractor selection pipeline
**Created**: 2026-08-20
**Feature**: [spec.md](../spec.md)
**Note**: This custom checklist is generated by the `/speckit-checklist` command based on feature context and requirements.
**Review Ownership**: This checklist is a reviewer-owned requirements-quality review artifact. Mark an item `[x]` only when the reviewer determines the requirements-quality criterion is satisfied.
**Marker Semantics**: `[x]` means the criterion has been reviewed and satisfied for requirements quality. It does not mean implementation work is complete.
---
## Text Normalization & Tokenization Quality
- [x] CHK001 Are Unicode NFKC normalization rules explicitly specified for multilingual text content? [Completeness, Spec §FR-007]
- [x] CHK002 Is HTML and Markdown tag stripping behavior defined to prevent accidental concatenation of neighboring words? [Clarity, Spec §FR-007]
- [x] CHK003 Are anchor text extraction rules for Markdown and HTML links documented unambiguously? [Clarity, Spec §FR-007]
- [x] CHK004 Is the tokenization behavior (Unicode alphanumeric tokens, punctuation exclusion, lowercase) completely specified? [Completeness, Spec §FR-007]
## Shingles & Consensus Metric Formulation
- [x] CHK005 Is the sliding window shingle size (5-tokens) and the fallback rule for short texts (< 5 tokens) explicitly defined? [Clarity, Spec §FR-008]
- [x] CHK006 Are the mathematical formulas for Coverage, Support, and F1 Score defined with explicit zero-division handling? [Measurability, Spec §FR-010]
- [x] CHK007 Is the threshold for a shingle to enter the Consensus set (presence in $\ge 2$ active candidates) unambiguously stated? [Clarity, Spec §FR-009]
## Decision & Tie-Breaking Hierarchy
- [x] CHK008 Is the technical tie threshold ($\le 0.03$) quantified with exact comparison semantics? [Clarity, Spec §FR-011]
- [x] CHK009 Is the tie-breaker preference for the smaller candidate (fewest shingles) explicitly constrained to candidates within the technical tie pool? [Consistency, Spec §FR-011]
- [x] CHK010 Is the zero-consensus fallback hierarchy (median of 3, maximum of 2, single candidate) completely specified without ambiguous gaps? [Coverage, Spec §FR-012]
- [x] CHK011 Is the final mandatory priority order (`newspaper4k` > `readability` > `trafilatura`) consistent across all tie scenarios? [Consistency, Spec §FR-011, §FR-012]
## Candidate State Transitions & Resilience
- [x] CHK012 Are the criteria distinguishing Usable, Degraded, and Unavailable candidates defined unambiguously? [Completeness, Spec §FR-005]
- [x] CHK013 Does the spec define the exact behavior and fallback when all 3 extractors are Unavailable? [Edge Case, Spec §FR-013]
- [x] CHK014 Does the spec define what occurs when degraded candidates exist but no usable candidates are present? [Coverage, Spec §FR-006]
## JSON Schema Integrity & Atomic I/O
- [x] CHK015 Are requirements explicit that 100% of pre-existing fields, structures, and article order must be preserved unchanged? [Completeness, Spec §FR-003, §FR-010]
- [x] CHK016 Is the output filename pattern `<original_name_without_extension>_selected.json` specified for default CLI execution? [Clarity, Spec §FR-016]
- [x] CHK017 Are atomic write requirements (temporary file + atomic replacement) defined to prevent partial or corrupted files on disk? [Non-Functional, Spec §FR-002, §FR-016]
- [x] CHK018 Is the behavior for recalculating an already present `selected_extractor` key explicitly specified? [Clarity, Spec §FR-014]
---
## Notes
- Mark items `[x]` only after review confirms the requirement-quality criterion is satisfied.
- Leave items unchecked when they still require clarification, correction, or reviewer evaluation.
- `/speckit-implement` reads checklist checkbox state as a gate and must not modify markers.
- `checklists/requirements.md` has a separate built-in lifecycle maintained by `/speckit-specify` and `/speckit-clarify`.
- Items are numbered sequentially (CHK001 - CHK018) for easy reference.
@@ -0,0 +1,36 @@
# Specification Quality Checklist: Deterministic Content Selection
**Purpose**: Validate specification completeness and quality before proceeding to planning
**Created**: 2026-08-20
**Feature**: [spec.md](../spec.md)
## Content Quality
- [x] No implementation details (languages, frameworks, APIs)
- [x] Focused on user value and business needs
- [x] Written for non-technical stakeholders
- [x] All mandatory sections completed
## Requirement Completeness
- [x] No [NEEDS CLARIFICATION] markers remain
- [x] Requirements are testable and unambiguous
- [x] Success criteria are measurable
- [x] Success criteria are technology-agnostic (no implementation details)
- [x] All acceptance scenarios are defined
- [x] Edge cases are identified
- [x] Scope is clearly bounded
- [x] Dependencies and assumptions identified
## Feature Readiness
- [x] All functional requirements have clear acceptance criteria
- [x] User scenarios cover primary flows
- [x] Feature meets measurable outcomes defined in Success Criteria
- [x] No implementation details leak into specification
## Notes
- All requirements are derived directly from PRD `docs/prd_deterministic_content_selection.md`.
- No ambiguity remains; all edge cases and tie-breaking hierarchies are fully specified.
- Ready for `/speckit-plan`.
@@ -0,0 +1,54 @@
# CLI Interface Contract: Deterministic Article Content Selection
**Branch**: `004-deterministic-content-selection` | **Date**: 2026-08-20 | **Spec**: [spec.md](../spec.md)
---
## 1. Command Syntax
```bash
python scripts/select_article_extractor.py <input_file> [-o OUTPUT] [--indent INDENT] [--verbose]
```
---
## 2. Arguments and Flags
| Argumento / Flag | Tipo | Obrigatório | Padrão | Descrição |
|---|---|:---:|---|---|
| `input_file` | `Path` (Posicional) | Sim | - | Caminho para o arquivo JSON contendo a coleção `articles` extraída. |
| `-o`, `--output` | `Path` | Não | `<input_file_without_ext>_selected.json` | Caminho do arquivo JSON de destino. Se omitido, grava no mesmo diretório com sufixo `_selected.json`. |
| `--indent` | `int` | Não | `2` | Número de espaços para indentação do JSON de saída. Use `0` para JSON compacto em linha única. |
| `-v`, `--verbose` | `flag` | Não | `False` | Exibe no `stderr` detalhes da pontuação e justificativa de escolha por artigo. |
---
## 3. Standard Streams (I/O)
- **`stdout`**:
- Emite o sumário operacional em JSON ou texto resumido ao término da execução:
```json
{
"status": "success",
"input_file": "out/river_plate_extracted.json",
"output_file": "out/river_plate_extracted_selected.json",
"total_articles": 20,
"distribution": {
"newspaper4k": 9,
"readability": 9,
"trafilatura": 2
}
}
```
- **`stderr`**:
- Mensagens de log, progresso da barra/processamento de artigos e erros de validação ou exceções.
---
## 4. Exit Codes
| Código | Significado | Comportamento |
|:---:|---|---|
| `0` | **Sucesso** | Todos os artigos foram processados e o arquivo final foi gravado atomicamente com sucesso. |
| `1` | **Erro de I/O ou JSON Inválido** | Arquivo não encontrado, JSON malformado ou permissão negada. Nenhum arquivo de saída é gerado. |
| `2` | **Erro de Validação de Estrutura** | Raiz não é objeto ou chave `articles` não é uma lista. Nenhum arquivo de saída é gerado. |
@@ -0,0 +1,80 @@
# JSON Schema Contract: Deterministic Article Content Selection
**Branch**: `004-deterministic-content-selection` | **Date**: 2026-08-20 | **Spec**: [spec.md](../spec.md)
---
## 1. Input JSON Schema
O arquivo de entrada deve conter uma lista de artigos sob a chave `articles`.
```json
{
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"required": ["articles"],
"properties": {
"articles": {
"type": "array",
"items": {
"type": "object",
"properties": {
"trafilatura": {
"type": "object",
"properties": {
"text": { "type": ["string", "null"] },
"error": { "type": ["string", "null"] }
}
},
"newspaper4k": {
"type": "object",
"properties": {
"text": { "type": ["string", "null"] },
"error": { "type": ["string", "null"] }
}
},
"readability": {
"type": "object",
"properties": {
"cleaned_text": { "type": ["string", "null"] },
"error": { "type": ["string", "null"] }
}
}
}
}
}
}
}
```
---
## 2. Output JSON Schema
O arquivo de saída mantém todos os campos, metadados e ordem originais, adicionando obrigatoriamente `selected_extractor`.
```json
{
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"required": ["articles"],
"properties": {
"articles": {
"type": "array",
"items": {
"type": "object",
"required": ["selected_extractor"],
"properties": {
"selected_extractor": {
"type": "string",
"enum": ["trafilatura", "newspaper4k", "readability"]
},
"trafilatura": { "type": "object" },
"newspaper4k": { "type": "object" },
"readability": { "type": "object" }
}
}
}
}
}
```
@@ -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"
```
@@ -0,0 +1,93 @@
# Implementation Plan: Deterministic Article Content Selection
**Branch**: `004-deterministic-content-selection` | **Date**: 2026-08-20 | **Spec**: [spec.md](spec.md)
**Input**: Feature specification from `specs/004-deterministic-content-selection/spec.md`
---
## Summary
Implementação do motor determinístico de seleção de extratores (`scripts/select_article_extractor.py`), capaz de consumir arquivos JSON consolidados com saídas do **Trafilatura**, **Newspaper4k** e **Readability**, aplicar normalização de texto, geração de shingles (5-tokens), pontuação $F_1$ baseada em consenso e regras de desempate técnico / hierárquico estritas, gerando um novo arquivo JSON enriquecido exclusivamente com a chave `selected_extractor` em cada artigo de forma não-destrutiva e atômica.
---
## Technical Context
**Language/Version**: Python 3.10+
**Primary Dependencies**: Standard Library (`json`, `re`, `unicodedata`, `html`, `argparse`, `dataclasses`, `pathlib`, `tempfile`, `os`)
**Storage**: Arquivos JSON locais no diretório `out/`
**Testing**: `pytest` com testes unitários e de integração cobrindo 100% dos casos de teste obrigatórios (CT-001 a CT-014)
**Target Platform**: Windows / Linux / macOS (Terminal CLI & Módulo Python)
**Project Type**: CLI tool & modular selection engine
**Performance Goals**: Processamento em lote de centenas de artigos em menos de 1 segundo (complexidade linear $O(N)$ em memória)
**Constraints**: 100% determinístico, 0 chamadas de rede, sem uso de LLMs ou embeddings, escrita atômica em disco
**Scale/Scope**: Lotes de 1 a 10.000+ artigos
---
## Constitution Check
*GATE: Must pass before Phase 0 research. Re-check after Phase 1 design.*
| Princípio | Avaliação | Status |
|---|---|---|
| **I. Library / Modular Design** | Módulo estruturado com funções puras e dataclasses desacopladas (`normalize_text`, `generate_shingles`, `calculate_consensus_metrics`, `select_best_candidate`, `process_batch`). | ✅ Aprovado |
| **II. CLI Interface** | CLI via `scripts/select_article_extractor.py` com flags descritivas, streams padronizados (`stdout` para resumo e `stderr` para logs/erros) e códigos de saída específicos. | ✅ Aprovado |
| **III. Test-First (NON-NEGOTIABLE)** | TDD com suíte automatizada em `tests/test_select_article_extractor.py` cobrindo todos os cenários (CT-001 a CT-014) e validação end-to-end com o arquivo real `out/river_plate_extracted.json`. | ✅ Aprovado |
| **IV. Integration Testing** | Testes de integração validando leitura, enriquecimento de `selected_extractor`, não-destrutividade de campos e escrita atômica. | ✅ Aprovado |
| **V. Simplicity & YAGNI** | Uso exclusivo da biblioteca padrão do Python, sem dependências adicionais pesadas. | ✅ Aprovado |
---
## Project Structure
### Documentation (this feature)
```text
specs/004-deterministic-content-selection/
├── spec.md # Especificação de requisitos funcionais e critérios
├── plan.md # Este plano de implementação (/speckit-plan)
├── research.md # Decisões técnicas e algoritmos (Phase 0)
├── data-model.md # Entidades e modelos de dados (Phase 1)
├── quickstart.md # Guia de validação e execução (Phase 1)
├── contracts/
│ ├── cli-contract.md # Contrato de linha de comando
│ └── json-schema.md # Esquemas JSON de entrada e saída
└── checklists/
└── requirements.md # Checklist de validação da especificação
```
### Source Code Layout
```text
scripts/
├── extract_google_news.py # Extrator RSS do Google News
├── extract_article_contents.py # Extrator multimotor de artigos
└── select_article_extractor.py # [NEW] Seletor determinístico de extrator por artigo
tests/
├── test_extract_google_news.py # Testes do extrator Google News
├── test_extract_article_contents.py # Testes do extrator multimotor
└── test_select_article_extractor.py # [NEW] Testes unitários e de integração do seletor
```
**Structure Decision**: Criação de `scripts/select_article_extractor.py` como ferramenta CLI e biblioteca modular autônoma, e `tests/test_select_article_extractor.py` contendo a suíte de testes de alta fidelidade aos requisitos do PRD.
---
## Implementation Phases
### Phase 0: Outline & Research *(Completed)*
- Normalização de texto via biblioteca padrão (`html.unescape`, `unicodedata.normalize('NFKC')`, regex Unicode).
- Estratégia de geração de shingles de 5 tokens e cálculo de $F_1$ sobre consenso compartilhado por $\ge 2$ motores.
- Regras de desempate técnico (`<= 0.03`), desempate sem consenso (mediana/máximo) e fallback prioritário (`newspaper4k` > `readability` > `trafilatura`).
- Documentado em [research.md](research.md).
### Phase 1: Design & Contracts *(Completed)*
- Modelos de dados e dataclasses estruturados em [data-model.md](data-model.md).
- Contratos de linha de comando e JSON schema definidos em [contracts/](contracts/).
- Guia prático de execução e validação estruturado em [quickstart.md](quickstart.md).
### Phase 2: Tasks & Execution *(Next Step via `/speckit-tasks`)*
- Criação das tarefas de implementação e testes orientados a TDD em `tasks.md`.
@@ -0,0 +1,66 @@
# Quickstart: Deterministic Article Content Selection
**Branch**: `004-deterministic-content-selection` | **Date**: 2026-08-20 | **Spec**: [spec.md](spec.md)
---
## 1. Pré-requisitos
- Python 3.10+
- Ambiente virtual configurado com dependências do projeto instaladas (`pip install -r requirements.txt`).
---
## 2. Execução Rápida via CLI
### Cenário 1: Selecionar o melhor extrator para uma extração existente
```bash
python scripts/select_article_extractor.py out/river_plate_extracted.json
```
**Resultado esperado**:
- Arquivo `out/river_plate_extracted_selected.json` gerado contendo todos os 20 artigos com a chave `selected_extractor` devidamente preenchida (`trafilatura`, `newspaper4k` ou `readability`).
- O arquivo original `out/river_plate_extracted.json` permanece inalterado.
### Cenário 2: Especificar caminho de saída customizado e modo verboso
```bash
python scripts/select_article_extractor.py out/river_plate_extracted.json -o out/meu_resultado.json --verbose
```
**Resultado esperado**:
- Logs detalhados no `stderr` mostrando as pontuações e a regra acionada (ex: `highest_score`, `technical_tie`, etc.).
---
## 3. Execução dos Testes Automatizados
Para rodar a suíte completa de testes unitários e de integração (cobrindo os casos CT-001 a CT-014):
```bash
pytest tests/test_select_article_extractor.py -v
```
---
## 4. Validação Programática / Uso como Módulo Python
```python
from scripts.select_article_extractor import select_article_extractor
article_data = {
"trafilatura": {"text": "El Club Atlético River Plate venció 2-0 anoche.", "error": None},
"newspaper4k": {
"text": "El Club Atlético River Plate venció 2-0 anoche en el Monumental.",
"error": None,
},
"readability": {
"cleaned_text": "El Club Atlético River Plate venció 2-0 anoche.",
"error": None,
},
}
result = select_article_extractor(article_data)
print("Extrator selecionado:", result.selected_extractor.value)
print("Motivo da escolha:", result.selection_reason)
# Output esperado:
# Extrator selecionado: newspaper4k (ou readability dependendo do desempate de shingles)
# Motivo da escolha: technical_tie_smallest_shingles (ou highest_score)
```
@@ -0,0 +1,107 @@
# Research & Architectural Decisions: Deterministic Content Selection
**Branch**: `004-deterministic-content-selection` | **Date**: 2026-08-20 | **Spec**: [spec.md](spec.md)
---
## 1. Text Normalization Pipeline
### Context
Cada extrator (Trafilatura, Newspaper4k e Readability) gera textos com diferentes resíduos de formatação (entidades HTML como `&amp;` ou `&nbsp;`, links no formato markdown `[texto](url)` ou tags `<a href="...">texto</a>`, variações de quebras de linha e pontuações). A comparação textual para consenso exige uma normalização uniforme, determinística e de alta performance.
### Decisions
1. **Decodificação de entidades HTML**: Utilizar `html.unescape()` da biblioteca padrão do Python.
2. **Remoção de imagens Markdown**: Expressão regular `re.compile(r'!\s*\[[^\]]*\]\([^)]*\)')` substituindo imagens Markdown por espaços para descartar marcação de mídia não-textual e evitar falsos consensos com legendas.
3. **Preservação de texto de links Markdown**: Expressão regular `re.compile(r'\[([^\]]+)\]\([^)]+\)')` substituindo links Markdown pelo texto âncora `\1`.
4. **Remoção de tags HTML**: Expressão regular `re.compile(r'<[^>]+>')` substituindo tags por espaços para evitar fusão acidental de palavras vizinhas.
5. **Normalização Unicode**: `unicodedata.normalize('NFKC', text)` para uniformizar caracteres compostos, ligaduras e variantes tipográficas.
6. **Conversão para minúsculas**: `.lower()` após NFKC.
7. **Colapso de espaços em branco**: `re.sub(r'\s+', ' ', text).strip()`.
8. **Tokenização**: Extração de sequências alfanuméricas com `re.findall(r'[\w]+', text, flags=re.UNICODE)`. Pontuações são descartadas naturalmente sem remoção semântica de palavras.
### Rationale
- 100% implementável com módulos padrão do Python (`re`, `unicodedata`, `html`), garantindo portabilidade em qualquer ambiente sem novas dependências externas.
- Complexidade linear $O(N)$ no tamanho do texto, com execução em frações de milissegundo por artigo.
### Alternatives Considered
- `BeautifulSoup` para strip de tags: Rejeitado por ser mais lento e desnecessário para textos já extraídos.
- `nltk` ou `spacy`: Rejeitados por adicionarem dependências pesadas, download de modelos e lentidão desnecessária para uma tarefa de tokenização alfanumérica pura.
---
## 2. 5-Token Shingles & Consensus Metrics
### Context
O algoritmo compara a sobreposição textual entre os candidatos ativos através de janelas deslizantes consecutivas de 5 tokens (shingles).
### Decisions
1. **Geração de Shingles**:
- Para um candidato com $T$ tokens ordenados $[t_0, t_1, \dots, t_{T-1}]$:
- Se $T \ge 5$: conjunto de tuplas de 5 tokens $\{ (t_i, t_{i+1}, t_{i+2}, t_{i+3}, t_{i+4}) \mid 0 \le i \le T-5 \}$.
- Se $1 \le T \le 4$: conjunto contendo uma única tupla com todos os tokens $\{ (t_0, \dots, t_{T-1}) \}$.
- Se $T = 0$: conjunto vazio $\emptyset$.
2. **Construção do Consenso**:
- Para cada shingle único observado nos candidatos ativos, conta-se em quantos candidatos distintos ele aparece.
- $\text{Consenso} = \{ s \mid \text{contagem}(s) \ge 2 \}$.
3. **Métricas por Candidato Ativo $C$**:
- $\text{cobertura}(C) = \frac{|C_{\text{shingles}} \cap \text{Consenso}|}{|\text{Consenso}|}$
- $\text{suporte}(C) = \frac{|C_{\text{shingles}} \cap \text{Consenso}|}{|C_{\text{shingles}}|}$
- $\text{score}(C) = \frac{2 \times \text{cobertura}(C) \times \text{suporte}(C)}{\text{cobertura}(C) + \text{suporte}(C)}$ (se denominador for zero, $\text{score} = 0.0$).
### Rationale
- A métrica de pontuação $F_1$ penaliza tanto extratores que perderam conteúdo essencial (baixa cobertura) quanto extratores que trouxeram excesso de lixo/boilerplate do site (baixo suporte).
- A representação por `set` de tuplas em Python permite operações de intersecção (`&`) com complexidade ótima de tempo $O(|C|)$.
---
## 3. Regras de Decisão, Empate Técnico e Desempate Hierárquico
### Context
O sistema precisa garantir uma escolha única e determinística em todas as variações possíveis de entrada.
### Decisions
1. **Formação do Conjunto Ativo**:
- Classificação:
- `Usável`: `text` é string não vazia após normalização e `error` é `None`/vazio.
- `Degradado`: `text` é string não vazia após normalização, mas `error` não é `None`.
- `Indisponível`: `text` é nulo, ausente, não-string ou vazio.
- Se houver $\ge 1$ Usável $\to$ Ativos = Usáveis.
- Senão, se houver $\ge 1$ Degradado $\to$ Ativos = Degradados.
- Senão $\to$ Seleciona `newspaper4k` diretamente (Fallback Final).
- Se $|\text{Ativos}| = 1 \to$ Seleciona o único candidato ativo imediatamente.
2. **Seleção Com Consenso ($|\text{Consenso}| > 0$)**:
- Maior score $S_{\max} = \max_{C \in \text{Ativos}} \text{score}(C)$.
- Grupo de empate técnico: $\{ C \in \text{Ativos} \mid S_{\max} - \text{score}(C) \le 0.03 + 10^{-9} \}$.
- Se grupo tiver 1 candidato $\to$ Seleciona ele.
- Se grupo tiver $\ge 2$ candidatos $\to$ Seleciona o candidato com menor $|C_{\text{shingles}}|$ (menor conteúdo excedente).
- Se ainda houver empate no número de shingles $\to$ Desempate por prioridade fixa: `newspaper4k` > `readability` > `trafilatura`.
3. **Seleção Sem Consenso ($|\text{Consenso}| = 0$)**:
- Se $|\text{Ativos}| = 3 \to$ Seleciona candidato com quantidade **mediana** de shingles.
- Se $|\text{Ativos}| = 2 \to$ Seleciona candidato com **maior** quantidade de shingles.
- Se $|\text{Ativos}| = 1 \to$ Seleciona o único candidato.
- Empates na quantidade de shingles $\to$ Prioridade fixa: `newspaper4k` > `readability` > `trafilatura`.
### Rationale
- Total aderência às seções 7.1 a 7.6 do PRD. A tolerância de $10^{-9}$ evita imprecisões de ponto flutuante em comparações `<= 0.03`.
---
## 4. Estratégia de I/O Não Destrutiva e Escrita Atômica
### Context
O processamento em lote deve preservar a ordem dos artigos e todos os campos originais do JSON, gravando o resultado sem risco de corrupção de arquivos em caso de interrupção.
### Decisions
1. **Entrada e Saída**:
- Nome padrão de saída: `<nome_original_sem_extensão>_selected.json`.
- Suporte a argumento opcional de saída `--output / -o`.
2. **Gravação Atômica**:
- Gravar os dados em um arquivo temporário no mesmo diretório (`<saida>.tmp.<pid>`).
- Executar substituição atômica via `os.replace(temp_path, target_path)`.
3. **Preservação de Conteúdo**:
- Carregar o JSON original em estruturas nativas de dicionário/lista.
- Inserir a chave `selected_extractor` diretamente em cada dicionário de artigo.
- Se `selected_extractor` já existir na entrada, sobrescrever com o novo valor recalculado.
### Rationale
- Garante integridade absoluta dos dados contra falhas de disco ou encerramentos abruptos.
@@ -0,0 +1,135 @@
# Feature Specification: Deterministic Content Selection
**Feature Branch**: `004-deterministic-content-selection`
**Created**: 2026-08-20
**Status**: Draft
**Input**: User description: "usando o PRD: docs/prd_deterministic_content_selection.md"
## User Scenarios & Testing *(mandatory)*
### User Story 1 - Deterministic Selection with Text Consensus (Priority: P1)
As a data pipeline consumer or analyst, I want the system to automatically analyze the extracted text from Trafilatura, Newspaper4k, and Readability for each article and pick the single best extractor using consensus and coverage scoring, so that our dataset has high-quality, standardized content without human review.
**Why this priority**: Core value of the feature. Resolves the primary dilemma of choosing between 3 extractor outputs per article based on mutual agreement (consensus shingles) and concise content.
**Independent Test**: Can be tested independently by running the selection algorithm on articles where extractors have high agreement or partial variations, verifying that the extractor with highest F1 score (or closest score with fewest excess shingles) is selected.
**Acceptance Scenarios**:
1. **Given** an article with usable extracts from all 3 libraries where 2 or 3 libraries agree closely, **When** selection is evaluated, **Then** the library with the highest consensus F1-score (or the more concise candidate within a 0.03 technical tie margin) is set in `selected_extractor`.
2. **Given** an extractor with excess boilerplate/noise and two extractors with clean common content, **When** selection is evaluated, **Then** the noisy extractor suffers lower support score and the clean agreeing extractor is selected.
3. **Given** an extractor with only a small snippet and two extractors with complete text, **When** selection is evaluated, **Then** the short snippet loses due to low consensus coverage.
---
### User Story 2 - Resilient Decision Under Total Disagreement or Degradation (Priority: P2)
As a pipeline maintainer, I want the selection algorithm to make a deterministic and sensible fallback choice even when extractors completely disagree, produce errors, or return empty/degraded content, so that the pipeline never halts or leaves an article without a chosen extractor.
**Why this priority**: Essential for pipeline stability. The system must guarantee that every article gets an unambiguous winner without throwing runtime exceptions or generating `null`/`ambiguous` states.
**Independent Test**: Can be tested with synthetic articles representing edge cases: all extractors returning non-overlapping text, extractors reporting errors, or all extractors failing.
**Acceptance Scenarios**:
1. **Given** 3 active candidates with 0 consensus shingles, **When** selection runs, **Then** the candidate with the median shingle length is selected.
2. **Given** 2 active candidates with 0 consensus shingles, **When** selection runs, **Then** the candidate with the larger shingle count is selected.
3. **Given** an article where all usable candidates are absent but degraded candidates exist, **When** selection runs, **Then** the algorithm evaluates only the degraded candidates.
4. **Given** an article where all 3 extractors failed or returned empty content, **When** selection runs, **Then** `newspaper4k` is selected via the mandatory final fallback rule.
---
### User Story 3 - Non-Destructive JSON Batch Processing (Priority: P3)
As a system operator, I want to pass a JSON file with an `articles` array, execute the deterministic selector, and receive a new file `<original_name>_selected.json` with all original data and order intact plus the `selected_extractor` field, leaving the original file completely untouched.
**Why this priority**: Guarantees data preservation, idempotency, and clean pipeline integration.
**Independent Test**: Can be tested by running the process on a full batch JSON file (such as `out/river_plate_extracted.json`) and comparing input vs output keys, element counts, article order, and field contents.
**Acceptance Scenarios**:
1. **Given** a valid JSON file with $N$ articles, **When** the batch selection is executed, **Then** a new file `<original_name>_selected.json` is generated containing exactly $N$ articles in identical order, each with all original fields plus `selected_extractor`.
2. **Given** an input JSON file where `selected_extractor` already exists, **When** the batch selection is executed, **Then** `selected_extractor` is recalculated and updated.
3. **Given** an invalid JSON file or a file where `articles` is not a list, **When** execution runs, **Then** the process terminates with an error and does not produce a partial or corrupted output file.
---
### Edge Cases
- **Empty `articles` list (`[]`)**: Produces a valid output JSON containing an empty `articles: []` list without errors.
- **Exact score & shingle count tie**: Resolved deterministically by the strict fallback hierarchy: `newspaper4k` > `readability` > `trafilatura`.
- **Single active candidate**: When only 1 library produces usable output, it is selected immediately without computing consensus.
- **Short texts (< 5 tokens)**: When candidate text has between 1 and 4 tokens, the entire token sequence forms a single shingle.
- **Malformed fields / Type mismatch**: If a content field is not a string or missing, the candidate is classified as unavailable.
- **Missing library block**: If an article does not contain a `trafilatura`, `newspaper4k`, or `readability` block, that candidate is treated as unavailable.
## Requirements *(mandatory)*
### Functional Requirements
- **FR-001**: System MUST accept a valid JSON file path containing a root object with an `articles` array.
- **FR-002**: System MUST validate input structure (root is object, `articles` is list) and terminate immediately without creating an output file if validation fails.
- **FR-003**: System MUST process all articles in `articles`, preserving their exact sequence and all existing fields and values without modification.
- **FR-004**: System MUST evaluate extractor candidates using exclusively:
- `trafilatura.text` for Trafilatura
- `newspaper4k.text` for Newspaper4k
- `readability.cleaned_text` for Readability
- **FR-005**: System MUST categorize each candidate into one of three states:
- *Usable*: Content is non-empty string after normalization and extractor `error` is null/empty.
- *Degraded*: Content is non-empty string after normalization but extractor `error` is non-null.
- *Unavailable*: Content is missing, not a string, or empty after normalization.
- **FR-006**: System MUST form the active candidate set per article: Usable candidates if any exist; otherwise Degraded candidates if any exist; otherwise trigger final fallback.
- **FR-007**: System MUST perform deterministic in-memory normalization for candidate comparisons:
1. Decode HTML entities.
2. Strip Markdown images (`![alt](url)`), removing non-textual media embeds.
3. In Markdown links (`[text](url)`), preserve anchor text and strip URL targets.
4. Strip HTML tags, maintaining spacing between adjacent words.
5. Apply Unicode NFKC normalization.
6. Convert to lowercase.
7. Collapse multiple whitespace/newlines/tabs into a single space.
8. Tokenize retaining Unicode letters and digits.
9. Ignore punctuation symbols.
- **FR-008**: System MUST generate 5-token sliding window shingles from the ordered token sequence of each candidate (or single $N$-token shingle if $1 \le N \le 4$).
- **FR-009**: System MUST construct the consensus shingle set (shingles appearing in at least 2 active candidates).
- **FR-010**: System MUST compute `coverage`, `support`, and `score` ($F_1 = 2 \times \text{coverage} \times \text{support} / (\text{coverage} + \text{support})$) for each active candidate against the consensus shingles (or 0 if denominator is 0).
- **FR-011**: When consensus shingles exist, the system MUST:
1. Sort candidates descending by score.
2. Identify all candidates within a `0.03` difference from the top score (technical tie pool).
3. If technical tie pool has 1 candidate, select it.
4. If multiple candidates are in technical tie, select the one with the smallest total shingle count (least surplus).
5. If shingle count is also tied, apply priority hierarchy: `newspaper4k` > `readability` > `trafilatura`.
- **FR-012**: When 0 consensus shingles exist, the system MUST:
- With 3 active candidates: select candidate with median shingle count.
- With 2 active candidates: select candidate with maximum shingle count.
- With 1 active candidate: select that single candidate.
- In shingle count ties: apply priority hierarchy (`newspaper4k` > `readability` > `trafilatura`).
- **FR-013**: When 0 active candidates exist (all unavailable), system MUST assign `newspaper4k`.
- **FR-014**: System MUST inject or replace `selected_extractor` in each article item with exactly one value from `{"trafilatura", "newspaper4k", "readability"}`.
- **FR-015**: System MUST never output `null`, empty string, `ambiguous`, or leave an article without a selection.
- **FR-016**: System MUST write the result atomically to `<original_name_without_extension>_selected.json` in the same directory or specified target, leaving the input file unchanged.
- **FR-017**: System MUST produce 100% deterministic and identical outputs across repeated runs with identical inputs.
### Key Entities *(include if feature involves data)*
- **Article Input Batch**: Root JSON container with metadata and an ordered list of `articles`.
- **Article Record**: Object representing an article, containing source metadata, extraction results from the 3 extractors (`trafilatura`, `newspaper4k`, `readability`), and the resulting `selected_extractor` tag.
- **Extractor Candidate**: Evaluation model for an individual extractor containing raw text, error state, candidate usability state (`Usable`, `Degraded`, `Unavailable`), normalized token stream, 5-token shingles, and computed metrics (`coverage`, `support`, `score`, `shingle_count`).
- **Consensus Shingle Set**: Set of unique 5-token shingles shared by 2 or more active extractor candidates.
## Success Criteria *(mandatory)*
### Measurable Outcomes
- **SC-001**: **100% Selection Completeness**: 100% of articles in the input collection receive a valid `selected_extractor` value from the closed set `['trafilatura', 'newspaper4k', 'readability']`.
- **SC-002**: **0% Ambiguity**: Exactly 0 articles result in `null`, missing, empty, or ambiguous selection states.
- **SC-003**: **100% Deterministic Reproducibility**: 100% identical `selected_extractor` values when executing across multiple runs on identical input datasets.
- **SC-004**: **100% Non-Destructive Integrity**: 100% of pre-existing keys, nested objects, article counts, and article ordering are preserved identically in the output JSON.
- **SC-005**: **100% Test Case Coverage**: Passes 100% of defined mandatory test cases (CT-001 through CT-014).
- **SC-006**: **Atomic Operation**: 0 partial or corrupted output files generated on process failure or invalid JSON inputs.
## Assumptions
- The input JSON is generated by the extraction pipeline and contains `articles` where each item may have `trafilatura`, `newspaper4k`, and `readability` sub-objects.
- All three extraction libraries operated on the exact same HTML source document.
- No external dependencies (LLM APIs, embedding services, or network calls) are permitted during the selection process.
- Unicode NFKC normalization and standard tokenization cover multilingual article content (e.g. Portuguese, Spanish, English).
- Default output file path naming convention `<name>_selected.json` is sufficient, with CLI support for optional custom destination.

Some files were not shown because too many files have changed in this diff Show More