Files
pastor-online/docs/superpowers/plans/2026-11-01-pastor-online.md
T
omp ebf430c535 docs: anotar plano como superado e registrar achados medidos da busca
README e .env.example passam a registrar, com os números medidos no índice
real de 31.098 versículos: o stemming (Mt 5:44 de 434º a 1º no BM25), o BM25
sobre o versículo em vez da janela (12º a 1º), a cota MAX_KEYWORD_ONLY e os dois
limites que nenhum ajuste de nota resolve — MIN_SIMILARITY não bloqueia off-topic
(0/6 em qualquer limiar de 0,30 a 0,50) e a nota BM25 não separa acerto de ruído
(Dt 28:27 a 1,000 do topo contra Lc 7:47 a 0,716).
2026-11-02 13:18:49 -03:00

11 KiB

Pastor Online Implementation Plan

Superado em 2026-11-02. As tarefas de ingestão deste plano (PDF → OCR → parser de índice → reconstrução de capítulo/versículo) foram substituídas por um corpus USFX em domínio público. O código correspondente (extraction.py, bible_parser.py, Tesseract, PyMuPDF) foi removido. Leia a atualização no topo de docs/superpowers/specs/2026-11-01-pastor-online-design.md antes de seguir qualquer passo abaixo.

For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.

Goal: Entregar um sistema web RAG "Pastor Online" containerizado, que indexa PDFs da Bíblia (incluindo o PDF fornecido, que exige OCR) e responde em chat pastoral ancorado exclusivamente nos versículos recuperados.

Architecture: Monólito modular FastAPI + ChromaDB persistente + sentence-transformers multilíngue. Adaptadores HTTP próprios (httpx) para Ollama, OpenAI, Groq e Anthropic. Frontend React/Vite servido por Nginx, que fala SSE com o backend. Pipeline de ingestão: PyMuPDF → (fallback Tesseract) → parser bíblico → embeddings → Chroma, com cache de OCR por página e manifesto incremental por SHA-256.

Tech Stack: Python 3.11, FastAPI, ChromaDB, sentence-transformers, PyMuPDF, Tesseract, httpx, pytest; React 18, TypeScript, Vite, Vitest, Nginx, Docker Compose.

Spec: docs/superpowers/specs/2026-11-01-pastor-online-design.md

Global Constraints

  • Python 3.11 (python:3.11-slim); React 18 + TypeScript; Docker Compose único.
  • PDFs em ./PDF, montados read-only em /data/pdfs; ./pdfs suportado via PDF_DIR.
  • Vetores em /data/vector_db (volume nomeado vector_db), compartilhado com o cache de OCR.
  • System prompt pastoral é o da especificação, acrescido de escopo de crise (§10.2 do spec).
  • Citações no formato único Livro Capítulo:Versículo; validação antes de exibir (buffer completo).
  • Resposta sem evidência é fixa e não chama o LLM.
  • Nenhum segredo no bundle do frontend; chaves só no backend via env.
  • Reindexação concorrente → 409. REINDEX_TOKEN opcional protege POST /api/reindex.
  • Texto do corpus indexado é dado, nunca instrução (prompt injection).
  • OCR: 200 DPI, tesseract -l por+eng --psm 6; cache por (sha256, página).

Review Focus

  • PDF com camada textual corrompida (o arquivo real) ⇒ precisa cair no OCR e ainda assim produzir Gênesis 1:1 correto.
  • Pergunta com referência explícita (João 3:16) ⇒ resposta ancorada no versículo pedido, não no mais similar.
  • Resposta do LLM com citação inventada ⇒ citação removida antes de chegar à tela.
  • Índice do PDF ausente/ilegível ⇒ atribuição de livro por cabeçalho da página, sem perder o arquivo inteiro.
  • Reindexação com PDF alterado ⇒ só o arquivo alterado é reprocessado; o resto do índice permanece.
  • OCR de página que falha ⇒ arquivo fica partial, páginas com falha são reprocessadas depois, backend não cai.

Task 1: Fundação do backend — config, modelos e extração de PDF

Files:

  • Create: backend/requirements.txt
  • Create: backend/requirements-dev.txt
  • Create: backend/pyproject.toml
  • Create: backend/app/__init__.py
  • Create: backend/app/config.py
  • Create: backend/app/models.py
  • Create: backend/app/extraction.py
  • Test: backend/tests/test_extraction.py

Interfaces:

  • Produces: Settings (pydantic-settings, get_settings() -> Settings); PageText(page:int, text:str); PdfExtractor(cache_dir: Path, dpi:int=200, ocr_lang:str="por+eng", ocr_timeout:int=120) com legible_ratio(text:str)->float, extract_page(page, file_hash, page_number)->str, extract_document(path)->Iterator[PageText], ocr_page_image(pixmap)->str.

  • Step 1: Escrever backend/tests/test_extraction.py — legible_ratio alto para texto normal, baixo para controles C0; página legível não chama OCR; página corrompida chama OCR; cache reutilizado na segunda chamada; erro de OCR não propaga.

  • Step 2: pytest backend/tests/test_extraction.py -v → FAIL (módulo inexistente).

  • Step 3: Implementar config.py, models.py, extraction.py.

  • Step 4: pytest backend/tests/test_extraction.py -v → PASS.

  • Step 5: Commit.

Task 2: Parser bíblico

Files:

  • Create: backend/app/bible_parser.py
  • Test: backend/tests/test_bible_parser.py

Interfaces:

  • Consumes: PageText (Task 1).

  • Produces: VerseChunk(book, chapter, verse, page, file, text); BOOKS (66 livros canônicos em português); parse_book_index(text)->dict[str,int]; BibleParser.parse(pages, file_name, book_starts)->list[VerseChunk].

  • Step 1: Escrever testes — atribui livro por faixa de páginas; detecta [N]; reconstrói Gênesis 1:1; de-hifeniza esconde-\nme; número de versículo isolado vira versículo com texto na linha seguinte; página de índice/copyright fora da faixa é descartada.

  • Step 2: Rodar → FAIL.

  • Step 3: Implementar bible_parser.py (lista canônica + aliases; normalização sem acentos).

  • Step 4: Rodar → PASS.

  • Step 5: Commit.

Task 3: Embeddings e ChromaDB

Files:

  • Create: backend/app/embedding.py
  • Create: backend/app/vector_store.py
  • Test: backend/tests/test_vector_store.py

Interfaces:

  • Produces: Embedder protocol (encode(texts:list[str])->list[list[float]]), SentenceTransformerEmbedder(model_name), HashEmbedder (testes, determinístico); VectorStore(collection_name, persist_dir) com upsert(chunks, vectors), delete_file(file), query(vector, n_results, min_similarity)->list[SearchResult], get_by_reference(book, chapter, verse)->list[SearchResult], count()->int.

  • Step 1: Escrever testes — upsert + query; delete_file remove só do arquivo; filtro por referência; min_similarity descarta resultado abaixo do limiar; vetores normalizados.

  • Step 2: Rodar → FAIL.

  • Step 3: Implementar embedding.py e vector_store.py (Chroma com hnsw:space=cosine).

  • Step 4: Rodar → PASS.

  • Step 5: Commit.

Task 4: Indexador com manifesto e cache de OCR

Files:

  • Create: backend/app/indexer.py
  • Test: backend/tests/test_indexer.py

Interfaces:

  • Consumes: PdfExtractor, BibleParser, Embedder, VectorStore.

  • Produces: IndexStatus(state,pdfs_total,pdfs_indexed,chunks,last_indexed_at,errors); Indexer.reindex(force:bool=False)->IndexStatus; manifesto em /data/vector_db/manifest.json com estados complete|partial|error.

  • Step 1: Escrever testes — só o PDF com SHA alterado é reprocessado; manifestos persistidos; arquivo com página falha fica partial; count() reflete os chunks.

  • Step 2: Rodar → FAIL.

  • Step 3: Implementar indexer.py.

  • Step 4: Rodar → PASS.

  • Step 5: Commit.

Task 5: Adaptadores de LLM com streaming

Files:

  • Create: backend/app/llm.py
  • Test: backend/tests/test_llm.py

Interfaces:

  • Produces: LLMProvider protocol (async stream(messages, system)->AsyncIterator[str]); OllamaProvider, OpenAIProvider (cobre Groq via base_url), AnthropicProvider, StubProvider; build_provider(settings)->LLMProvider; SYSTEM_PROMPT com as diretrizes da spec + escopo de crise.

  • Step 1: Escrever testes — cada adaptador converte o formato do provedor em deltas de texto; build_provider escolhe pelo LLM_PROVIDER; erro HTTP vira LLMProviderError sem expor a chave.

  • Step 2: Rodar → FAIL.

  • Step 3: Implementar llm.py.

  • Step 4: Rodar → PASS.

  • Step 5: Commit.

Task 6: Chat — contexto, validação de citações e resposta pastoral

Files:

  • Create: backend/app/chat.py
  • Test: backend/tests/test_chat.py

Interfaces:

  • Produces: ChatService(settings, store, embedder, provider) com async answer(message, history)->ChatResult; ChatResult(text, sources, citations); NO_EVIDENCE_MESSAGE; validate_citations(text, allowed)->tuple[str,list[str]]; extract_reference(text)->tuple[str,int,int]|None.

  • Step 1: Escrever testes — sem evidência ⇒ texto fixo e provider não chamado; citação não sustentada é removida; sustentada é preservada; pergunta com João 3:16 usa o chunk exato; system prompt contém as 4 diretrizes da spec e o escopo de crise.

  • Step 2: Rodar → FAIL.

  • Step 3: Implementar chat.py.

  • Step 4: Rodar → PASS.

  • Step 5: Commit.

Task 7: API FastAPI

Files:

  • Create: backend/app/main.py
  • Test: backend/tests/test_main.py

Interfaces:

  • Produces: GET /api/health, GET /api/status, POST /api/reindex, POST /api/chat (SSE), GET /api/sources; create_app(settings=None, services=None) para testes.

  • Step 1: Escrever testes — status; reindex 202/409/token; chat 422 para mensagem longa; formato SSE sources → token → done; CORS restrito à origem configurada.

  • Step 2: Rodar → FAIL.

  • Step 3: Implementar main.py.

  • Step 4: Rodar → PASS.

  • Step 5: Commit.

Task 8: Frontend React

Files:

  • Create: frontend/package.json, frontend/tsconfig.json, frontend/vite.config.ts, frontend/index.html, frontend/Dockerfile, frontend/nginx.conf
  • Create: frontend/src/main.tsx, frontend/src/App.tsx, frontend/src/api.ts, frontend/src/styles.css, frontend/src/lib/sse.ts
  • Create: frontend/src/components/ChatPanel.tsx, MessageBubble.tsx, SourcesAccordion.tsx, StatusPanel.tsx
  • Test: frontend/src/lib/sse.test.ts, frontend/src/components/ChatPanel.test.tsx

Interfaces:

  • Consumes: API do backend (§8.2 do spec).

  • Produces: parseSSE(stream:ReadableStream<Uint8Array>):AsyncGenerator<SSEEvent>; sendMessage(body):AsyncGenerator<SSEEvent>.

  • Step 1: Escrever testes Vitest — parser SSE divide eventos; Enter envia, Shift+Enter quebra linha.

  • Step 2: npm test → FAIL.

  • Step 3: Implementar o frontend.

  • Step 4: npm test → PASS; npm run build → PASS.

  • Step 5: Commit.

Task 9: Docker Compose, env e README

Files:

  • Create: docker-compose.yml, .env.example, .dockerignore, README.md
  • Modify: backend/Dockerfile

Interfaces:

  • Produces: serviços backend, frontend, ollama, ollama-pull; volumes vector_db, ollama; COMPOSE_PROFILES=local padrão.

  • Step 1: Escrever README.md com preparación de .env, volume de PDFs, comandos de start, provedores, troubleshooting de OCR e de Ollama.

  • Step 2: docker compose config → exit 0.

  • Step 3: docker compose build → exit 0.

  • Step 4: Commit.

Task 10: Verificação de integração

  • Step 1: docker compose up -d e GET /api/health → 200.
  • Step 2: POST /api/reindex → 202; /api/status → indexing → ready.
  • Step 3: POST /api/chat com LLM_PROVIDER=stub → sources + token + done.
  • Step 4: Abrir o frontend no navegador e verificar chat, citações, accordion e painel de status.
  • Step 5: pytest backend/tests -v → 0 falhas; npm test → 0 falhas.