feat: initial commit - Jhonny Editor

- Adicionado estrutura completa do projeto
- Configurado MCP server para Premiere Pro
- Adicionado documentação e skills
- Configurado Gitignore para o projeto
This commit is contained in:
João Henrique
2026-09-08 09:59:31 -04:00
commit b541f502ba
1507 changed files with 387650 additions and 0 deletions
+11
View File
@@ -0,0 +1,11 @@
# Template — copiar para .env (fora do git) e preencher com valores reais.
# Valores reais e a explicação de como abrir o túnel SSH ficam em
# admin/VPS-ACCESS.md (nunca neste arquivo, nunca commitado).
# Host local do túnel SSH (não o IP da VPS — a conexão direta não é exposta)
RAG_DB_HOST=127.0.0.1
RAG_DB_PORT=55435
RAG_DB_NAME=rag_doza
RAG_DB_USER=rag_admin
RAG_DB_PASSWORD=
RAG_DB_SCHEMA=doza
+113
View File
@@ -0,0 +1,113 @@
# RAG deste projeto
Esta pasta é o **padrão Genial Sistemas** para dar a qualquer sistema (CRM,
Doza, contabilidade, o que vier depois) um banco de RAG próprio — usado só
para a IA indexar código/documentação e responder consultas gastando menos
tokens, sem precisar reler o repositório inteiro a cada tarefa.
**Não é o banco de dados do sistema.** É infraestrutura de apoio ao
desenvolvimento, mantida à parte da aplicação em si.
## Como funciona a infra
Existe **um único container Postgres + pgvector** compartilhado, rodando na
VPS da equipe (`rag-hub-db`, em `/docker/rag-hub/`). Cada sistema recebe o
seu **próprio banco de dados** dentro desse container — não um container
Docker novo por projeto. Isso é intencional: mais barato de manter (1
backup, 1 upgrade, 1 lugar pra olhar) do que uma stack isolada por sistema.
```
rag-hub-db (container único na VPS)
├── rag_doza ← banco deste projeto
├── rag_crm ← banco do CRM (quando existir)
├── rag_contabilidade ← banco da contabilidade (quando existir)
└── ... ← um banco por sistema novo
```
## Arquivos desta pasta
- `README.md` — este arquivo.
- `SETUP.md` — passo a passo para provisionar o banco deste projeto na
primeira vez (ou reprovisionar do zero).
- `schema.sql` / `schema-tigre.sql` — schema de cada sistema (extensões,
tabelas, índices). São o estado FINAL desejado: num banco novo basta
rodá-los. Num banco que já existe, use as migrations.
- `migrations/` — mudanças de schema numeradas e idempotentes, aplicadas com
`migrate_tigre.sh` (precisa de `rag_admin`; o usuário de indexação não tem
DDL). Cada arquivo abre com o motivo da mudança e o que fazer depois.
- `index_code.py` — indexador. `swift_chunker.py` faz o corte de arquivos
Swift por declaração.
- `search.py` — busca. `bench.py` mede recall e custo.
- `reindex_hook.sh` — hook de reindexação do arquivo recém-editado.
- `.env.example` — variáveis de ambiente que o código deste projeto precisa
para se conectar ao banco (sem valores reais — apenas o formato).
## Como a busca funciona
Três listas em paralelo, fundidas com RRF ponderado:
1. **densa** — embedding do trecho de código;
2. **lexical** — `pg_trgm` sobre os símbolos declarados, para consultas que
citam o nome exato de um tipo;
3. **resumo** — embedding só do doc-comment do arquivo, sem código para
diluir, que resgata arquivos pequenos e precisos.
Cada trecho guarda `start_line`/`end_line`, então o resultado aponta a janela
exata (`arquivo.swift:120-180`) em vez de mandar ler o arquivo inteiro.
Os números que justificam esse desenho estão em `bench.py` (18 consultas
douradas) e nos cabeçalhos das migrations. Antes: recall@5 de 33%. Depois:
94%, com 67% menos bytes por consulta. **Ao mexer nos parâmetros de fusão ou
no cortador, rode `bench.sh` antes e depois.**
### Modos de saída
| Comando | O que traz |
|---|---|
| `search_tigre.sh "consulta"` | caminho, faixa de linhas e uma linha de descrição (padrão) |
| `… --snippet` | + 300 chars do trecho |
| `… --full` | + o trecho inteiro |
| `… --json` | saída estruturada |
| `… --module X` / `--path Y` / `--ext .swift` | restringe o escopo |
| `… --map [termo]` | inventário de arquivos, sem nenhum código |
### Os dois bancos, lado a lado
O mesmo `search.py`/`index_code.py` atende `rag_tigre` (Swift) e `rag_doza`
(Python legado) — só muda o wrapper (`_tigre.sh` / `_doza.sh`) e o `.env`
carregado. `index_code.py` escolhe o cortador pela extensão:
`swift_chunker.py` para `.swift`, `python_chunker.py` para `.py`, janela de
linhas para o resto (`.md`, `.sql`, `.sh`).
O legado tem uma diferença estrutural real: não segue "uma classe por
arquivo" (`ai_analysis.py` e `app.py` passam de 5000 linhas, com dezenas de
funções soltas), então o `python_chunker` desce um nível dentro de blocos
grandes demais em vez de aceitar o arquivo inteiro como um só trecho.
Números do legado em `rag/bench_doza.sh`: recall@5 de 72% (18 consultas).
Mais baixo que os 94% do Tigre — a causa identificada não é bug do
pipeline, é o modelo de embedding (`nomic-embed-text`, majoritariamente
inglês) perdendo para conteúdo em português que soa tematicamente próximo:
"transcrever o áudio para texto" caiu atrás de markdown em português sobre
edição de voz, na frente do próprio `transcribe.py` (docstring em inglês).
Um modelo multilíngue deve fechar essa lacuna — ver README no que muda ao
trocar de modelo antes de decidir.
## Onde ficam as credenciais reais
Nunca neste diretório (ele é comitado no git). Credenciais reais —
host da VPS, senha do Postgres, chave SSH — ficam em `admin/VPS-ACCESS.md`
na raiz do projeto (nunca comitado, `chmod 600`).
## Convenção para um sistema novo
Ao começar um sistema novo (ou adicionar RAG a um já existente):
1. Copiar esta pasta `rag/` inteira para a raiz do novo projeto.
2. Trocar todas as referências de `doza`/`rag_doza` pelo nome do novo
sistema (ex: `crm` → banco `rag_crm`).
3. Seguir `SETUP.md` para criar o banco na VPS (usa a mesma `rag-hub-db`,
não sobe container novo).
4. Preencher `admin/VPS-ACCESS.md` do novo projeto com as credenciais reais
(copiando o padrão de acesso SSH/túnel já documentado nos projetos
existentes).
+82
View File
@@ -0,0 +1,82 @@
# Setup do banco RAG — Doza
Pré-requisito: o container compartilhado `rag-hub-db` já precisa estar
rodando na VPS (`/docker/rag-hub/`, ver `admin/VPS-ACCESS.md`). Se ele ainda
não existe, esse é um passo único para toda a infra, não deste projeto —
consulte quem já configurou antes de recriar.
## 1. Abrir túnel SSH até o Postgres
O banco não tem porta pública. Toda conexão (local ou de outro projeto)
passa por túnel SSH:
```bash
ssh -N -L 55435:127.0.0.1:55435 root@179.197.228.240
```
Mantenha esse comando rodando em um terminal enquanto for usar o banco.
As credenciais SSH (host, chave) estão em `admin/VPS-ACCESS.md`.
## 2. Criar o banco deste projeto (só na primeira vez)
Conectado via túnel, como usuário admin do `rag-hub-db` (`rag_admin`,
senha em `admin/VPS-ACCESS.md` na VPS):
```bash
psql -h 127.0.0.1 -p 55435 -U rag_admin -d postgres \
-c "CREATE DATABASE rag_doza OWNER rag_admin;"
```
> Se já existe (caso comum — este projeto já tem o banco criado), pule
> este passo.
## 3. Aplicar o schema
```bash
psql -h 127.0.0.1 -p 55435 -U rag_admin -d rag_doza -f rag/schema.sql
```
`schema.sql` é idempotente (`CREATE EXTENSION IF NOT EXISTS`,
`CREATE TABLE IF NOT EXISTS`) — rodar de novo não duplica nem apaga dados.
## 4. Conferir
```bash
psql -h 127.0.0.1 -p 55435 -U rag_admin -d rag_doza -c '\dx' -c '\dt doza.*'
```
Deve listar as extensões `vector` e `pg_trgm`, e a tabela `doza.code_chunks`.
## 5. Configurar o código do projeto
Copiar `.env.example` para `.env` (fora do git) e preencher com os valores
reais do túnel/credenciais — ver `admin/VPS-ACCESS.md`.
## Evoluir o schema (banco que já existe)
Não edite `schema.sql`/`schema-tigre.sql` esperando que sejam reaplicados —
eles descrevem o estado final, para bancos novos. Num banco existente, crie
uma migration numerada em `migrations/` e aplique:
```bash
rag/migrate_tigre.sh # a mais recente
rag/migrate_tigre.sh rag/migrations/003-outra.sql # uma específica
```
O script resolve a senha do `rag_admin` dentro da VPS — ela não passa pelo
shell local. Depois de qualquer migration que mexa em como o embedding é
gerado, reindexe com `rag/index_tigre.sh --full`, senão vetores de regimes
diferentes convivem no mesmo índice.
## Resetar do zero (se precisar)
```bash
psql -h 127.0.0.1 -p 55435 -U rag_admin -d postgres \
-c "DROP DATABASE rag_doza;" \
-c "CREATE DATABASE rag_doza OWNER rag_admin;"
psql -h 127.0.0.1 -p 55435 -U rag_admin -d rag_doza -f rag/schema.sql
```
⚠️ Isso apaga todo o índice/embeddings já gerados deste projeto — só é RAG
de apoio ao desenvolvimento, então é seguro recriar e reindexar do zero
quando fizer sentido (ex: mudança grande na estrutura do repo).
+284
View File
@@ -0,0 +1,284 @@
{
"summary": {
"label": "baseline-atual",
"mode": "map",
"top_k": 5,
"n": 18,
"recall@5": 0.444,
"top1": 0.167,
"latencia_media_s": 1.292,
"bytes_medios": 608
},
"rows": [
{
"query": "OAuthResourceServer",
"kind": "exato",
"expected": "src/oauth-resource-server.ts",
"rank": 1,
"elapsed": 1.3358233330000076,
"bytes": 666,
"got": [
"src/oauth-resource-server.ts",
"tests/oauth-resource-server.test.ts",
"src/oauth-resource-server.ts",
"src/resources/live-context-resources.ts",
"docs/recommendations/2026-08-19-round-3/47-resource-uri-policy.md"
]
},
{
"query": "UxpWebSocketBridge",
"kind": "exato",
"expected": "src/bridge/uxp-websocket-bridge.ts",
"rank": 2,
"elapsed": 1.2194245409991709,
"bytes": 811,
"got": [
"src/tools/uxp.ts",
"src/bridge/uxp-websocket-bridge.ts",
"src/tools/uxp-advanced-workflows.ts",
"src/tools/uxp-workflows.ts",
"src/tools/uxp-unique-identity-workflows.ts"
]
},
{
"query": "ProjectContextRepository",
"kind": "exato",
"expected": "src/context/project-context-store.ts",
"rank": null,
"elapsed": 1.3433856669944362,
"bytes": 598,
"got": [
"docs/project-context-engine.md",
"src/context/project-context-resource.ts",
"src/tools/project-context.ts",
"tests/project-context.test.ts",
"landing/app/project-intake/project-intake-prompts.tsx"
]
},
{
"query": "applyDoctorRepairPlan",
"kind": "exato",
"expected": "src/doctor-repairs.ts",
"rank": null,
"elapsed": 1.1002097079981468,
"bytes": 756,
"got": [
"docs/recommendations/2026-08-19-round-3/43-mrtr-roots-boundary.md",
"docs/industry/security-and-design-partner-pilot.md",
"docs/uxp-hybrid-addon-receipt.md",
"docs/recommendations/2026-08-18-round-2/22-mrtr-confirmation.md",
"docs/recommendations/2026-08-19-round-3/47-resource-uri-policy.md"
]
},
{
"query": "buildContentSecurityPolicy",
"kind": "exato",
"expected": "src/http-security.ts",
"rank": null,
"elapsed": 1.1801960420052637,
"bytes": 618,
"got": [
"src/security/capabilities.ts",
"tests/security-capabilities.test.ts",
"src/bridge/uxp-websocket-bridge.ts",
"src/resources/live-context-resources.ts",
"scripts/build-chat-dmg.sh"
]
},
{
"query": "planDerivedSilenceRemoval",
"kind": "exato",
"expected": "src/tools/silence-removal.ts",
"rank": null,
"elapsed": 1.258224417004385,
"bytes": 581,
"got": [
"product-growth-runs/premiere-pro-mcp/2026-08-22/00-executive-summary.md",
"product-growth-runs/premiere-pro-mcp/2026-08-22/04-design-creative-brief.md",
"product-growth-runs/premiere-pro-mcp/2026-08-22/06-paid-ads.md",
"docs/recommendations/2026-08-18/04-operation-scheduler.md",
"docs/30-day-launch-plan.md"
]
},
{
"query": "buildCaptionTimingPlan",
"kind": "exato",
"expected": "src/ai/caption-timing.ts",
"rank": null,
"elapsed": 1.046483124999213,
"bytes": 606,
"got": [
"product-growth-runs/premiere-pro-mcp/2026-08-22/04-design-creative-brief.md",
"product-growth-runs/premiere-pro-mcp/2026-08-22/00-executive-summary.md",
"landing/app/project-intake/project-intake-template-builder.tsx",
"scripts/build-chat-dmg.sh",
"product-growth-runs/premiere-pro-mcp/2026-08-22/08-approval-measurement.md"
]
},
{
"query": "emitAudit",
"kind": "exato",
"expected": "src/security/audit.ts",
"rank": null,
"elapsed": 1.0929507079999894,
"bytes": 565,
"got": [
"docs/mogrt-authoring.md",
"tests/http-admission.test.ts",
"tests/tools/uxp-object-mask-audit-workflows.test.ts",
"tests/uxp/object-mask-audit-workflows.test.ts",
"tests/tools/mogrt-authoring.test.ts"
]
},
{
"query": "cabecalhos de seguranca HTTP e CSP",
"kind": "conceitual",
"expected": "src/http-security.ts",
"rank": null,
"elapsed": 1.0924539580009878,
"bytes": 522,
"got": [
"docs/panel-ui-guidelines.md",
"tests/mcp-2026-protocol.test.ts",
"src/http-server.ts",
"docs/quickstart/es.md",
"cep-plugin/CSInterface.js"
]
},
{
"query": "registrar evento de telemetria de ativacao",
"kind": "conceitual",
"expected": "src/telemetry.ts",
"rank": 1,
"elapsed": 3.3157312920011464,
"bytes": 553,
"got": [
"src/telemetry.ts",
"docs/marketing/mcp-registry-readiness.md",
"docs/recommendations/2026-08-18-round-2/30-host-capability-attestation.md",
"docs/panel-ui-guidelines.md",
"tests/http-admission.test.ts"
]
},
{
"query": "ponte websocket com o plugin UXP",
"kind": "conceitual",
"expected": "src/bridge/uxp-websocket-bridge.ts",
"rank": 1,
"elapsed": 1.4117406250006752,
"bytes": 665,
"got": [
"src/bridge/uxp-websocket-bridge.ts",
"docs/panel-ui-guidelines.md",
"src/tools/uxp.ts",
"uxp-plugin/manifest.json",
"src/tools/uxp-unique-identity-workflows.ts"
]
},
{
"query": "interpretar arquivo de legenda SRT ou VTT",
"kind": "conceitual",
"expected": "src/ai/caption-timing.ts",
"rank": 3,
"elapsed": 1.3114452500012703,
"bytes": 514,
"got": [
"docs/quickstart/es.md",
"docs/panel-ui-guidelines.md",
"src/ai/caption-timing.ts",
"src/telemetry.ts",
"tests/tools/uxp-timeline-source-label-workflows.test.ts"
]
},
{
"query": "plano de remocao de silencio derivado da transcricao",
"kind": "conceitual",
"expected": "src/tools/silence-removal.ts",
"rank": null,
"elapsed": 1.0189579159996356,
"bytes": 557,
"got": [
"docs/quickstart/es.md",
"src/tools/edit-plans.ts",
"docs/panel-ui-guidelines.md",
"src/tools/editorial-plans.ts",
"src/tools/transcript-edits.ts"
]
},
{
"query": "armazenar contexto do projeto em disco",
"kind": "conceitual",
"expected": "src/context/project-context-store.ts",
"rank": null,
"elapsed": 1.0202497089994722,
"bytes": 647,
"got": [
"docs/quickstart/es.md",
"docs/panel-ui-guidelines.md",
"src/tools/project-context.ts",
"docs/recommendations/2026-08-18/16-context-delta-capture.md",
"docs/recommendations/2026-08-19-round-3/44-resource-context-annotations.md"
]
},
{
"query": "checagem e reparo automatico de instalacao (doctor)",
"kind": "conceitual",
"expected": "src/doctor-repairs.ts",
"rank": 5,
"elapsed": 1.1331077919967356,
"bytes": 538,
"got": [
"docs/panel-ui-guidelines.md",
"docs/doctor-repair-plans.md",
"docs/quickstart/es.md",
"docs/recommendations/2026-08-19-round-3/48-c2pa-inspection-lab.md",
"src/doctor-repairs.ts"
]
},
{
"query": "relatorio de intake do projeto",
"kind": "conceitual",
"expected": "src/intake/project-intake.ts",
"rank": 2,
"elapsed": 1.2240183749963762,
"bytes": 508,
"got": [
"docs/industry/project-intake-workflow.md",
"src/intake/project-intake.ts",
"docs/quickstart/es.md",
"docs/project-intake-host-report.schema.json",
"landing/app/project-intake/project-intake-prompts.tsx"
]
},
{
"query": "servidor OAuth para autenticacao de recursos",
"kind": "conceitual",
"expected": "src/oauth-resource-server.ts",
"rank": 2,
"elapsed": 1.0543467500028783,
"bytes": 681,
"got": [
"tests/oauth-resource-server.test.ts",
"src/oauth-resource-server.ts",
"docs/panel-ui-guidelines.md",
"docs/recommendations/2026-08-18-round-2/29-uxp-api-era-adapter.md",
"docs/recommendations/2026-08-18/03-auth-scoped-throttling.md"
]
},
{
"query": "registrar operacao de auditoria de seguranca",
"kind": "conceitual",
"expected": "src/security/audit.ts",
"rank": null,
"elapsed": 1.0914140829991084,
"bytes": 566,
"got": [
"docs/panel-ui-guidelines.md",
"docs/recommendations/2026-08-19-round-3/48-c2pa-inspection-lab.md",
"docs/recommendations/2026-08-18/19-object-mask-audit.md",
"security_best_practices_report.md",
"docs/marketing/mcp-registry-readiness.md"
]
}
]
}
+244
View File
@@ -0,0 +1,244 @@
{
"summary": {
"label": "baseline-atual",
"mode": "500chars",
"top_k": 5,
"n": 18,
"recall@5": 0.333,
"top1": 0.222,
"latencia_media_s": 0.364,
"bytes_medios": 1759
},
"rows": [
{
"query": "FCPXMLValidator",
"kind": "exato",
"expected": "TigreVideoEditing/FCPXMLValidator.swift",
"rank": null,
"elapsed": 0.43084554199595004,
"bytes": 2897,
"got": [
"TigreVideoEditing/FCPXMLReader.swift",
"TigreAppUI/WizardView.swift",
"TigreVideoEditing/XMLElementLookup.swift",
"TigreFoundation/Models/Project.swift",
"TigreAppUI/WizardModel.swift"
]
},
{
"query": "SilenceCutPipeline",
"kind": "exato",
"expected": "TigreVideoEditing/SilenceCutPipeline.swift",
"rank": 3,
"elapsed": 0.44454937500995584,
"bytes": 2896,
"got": [
"TigreAppUI/WizardModel.swift",
"TigreAppUI/WizardModel.swift",
"TigreVideoEditing/SilenceCutPipeline.swift",
"TigreAI/SilenceCutPreferencesStore.swift",
"TigreAppUI/WizardModel.swift"
]
},
{
"query": "TranscriptIndexBuilder",
"kind": "exato",
"expected": "TigreTranscription/Pipeline/TranscriptIndexBuilder.swift",
"rank": 2,
"elapsed": 0.47273016700637527,
"bytes": 2909,
"got": [
"TigreTranscription/Models/TranscriptResult.swift",
"TigreTranscription/Pipeline/TranscriptIndexBuilder.swift",
"TigreTranscription/Models/TranscriptSegment.swift",
"TigreApp/Web/TigreWebService.swift",
"TigreAppUI/WizardModel.swift"
]
},
{
"query": "SQLiteProjectStore",
"kind": "exato",
"expected": "TigreFoundation/Persistence/SQLiteProjectStore.swift",
"rank": null,
"elapsed": 0.37244337500305846,
"bytes": 573,
"got": [
"TigreAppUI/WizardView.swift"
]
},
{
"query": "EditPromptBuilder",
"kind": "exato",
"expected": "TigreAI/EditPromptBuilder.swift",
"rank": null,
"elapsed": 0.3064631669985829,
"bytes": 0,
"got": []
},
{
"query": "UniformFrameScheduler",
"kind": "exato",
"expected": "TigreVideoAnalysis/Pipeline/UniformFrameScheduler.swift",
"rank": null,
"elapsed": 0.38721733300189953,
"bytes": 1754,
"got": [
"TigreVideoEditing/FCPXMLReader.swift",
"TigreVideoEditing/FCPXMLValidator.swift",
"TigreVideoEditing/FCPXMLRoleScanner.swift"
]
},
{
"query": "TemplateRenderer",
"kind": "exato",
"expected": "TigreFoundation/Web/TemplateRenderer.swift",
"rank": 1,
"elapsed": 0.3559673750132788,
"bytes": 2332,
"got": [
"TigreFoundation/Web/TemplateRenderer.swift",
"TigreFoundation/Web/TemplateRenderer.swift",
"TigreAppUI/ProjectFormView.swift",
"TigreAppUI/ProjectFormView.swift"
]
},
{
"query": "como o wizard decide qual e o proximo passo",
"kind": "conceitual",
"expected": "TigreAppUI/WizardStep.swift",
"rank": 1,
"elapsed": 0.31415845798619557,
"bytes": 2789,
"got": [
"TigreAppUI/WizardStep.swift",
"TigreAppUI/WizardModel.swift",
"TigreAppUI/WizardEditDecisionReporter.swift",
"TigreApp/Web/WebRunner.swift",
"TigreAppUI/ProjectFormView.swift"
]
},
{
"query": "extrair o audio de um video com ffmpeg",
"kind": "conceitual",
"expected": "TigreTranscription/Pipeline/FFmpegAudioExtractor.swift",
"rank": 1,
"elapsed": 0.37834116601152346,
"bytes": 2988,
"got": [
"TigreTranscription/Pipeline/FFmpegAudioExtractor.swift",
"TigreAI/AppleIntelligenceMediaClassifier.swift",
"TigreVideoAnalysis/Pipeline/MediaAssetProbing.swift",
"TigreTranscription/Pipeline/FFmpegAudioExtractor.swift",
"TigreVideoAnalysis/Pipeline/AppleVisionAnalyzer.swift"
]
},
{
"query": "detectar trechos de silencio no audio",
"kind": "conceitual",
"expected": "TigreVideoEditing/SilenceAnalyzer.swift",
"rank": null,
"elapsed": 0.3787743330030935,
"bytes": 1162,
"got": [
"TigreVideoEditing/SilenceCutPipeline.swift",
"TigreAppUI/ContentView.swift"
]
},
{
"query": "baixar o modelo do whisper",
"kind": "conceitual",
"expected": "TigreTranscription/Pipeline/WhisperModelDownloader.swift",
"rank": null,
"elapsed": 0.30058345799625386,
"bytes": 0,
"got": []
},
{
"query": "escrever o arquivo final de FCPXML",
"kind": "conceitual",
"expected": "TigreVideoEditing/FCPXMLWriter.swift",
"rank": null,
"elapsed": 0.3599663749919273,
"bytes": 2762,
"got": [
"TigreVideoEditing/FCPXMLRoleScanner.swift",
"TigreVideoEditing/FCPXMLValidator.swift",
"TigreAI/SilenceCutExportService.swift",
"TigreAppUI/WizardModel.swift",
"TigreVideoEditing/FCPXMLValidationError.swift"
]
},
{
"query": "detectar rostos nos quadros do video",
"kind": "conceitual",
"expected": "TigreVideoAnalysis/Pipeline/FacePerceiver.swift",
"rank": null,
"elapsed": 0.36523920799663756,
"bytes": 2898,
"got": [
"TigreAI/SceneEditContext.swift",
"TigreAppUI/WizardModel.swift",
"TigreVideoAnalysis/Models/FaceObservation.swift",
"TigreAI/EditPromptBuilder.swift",
"TigreAppUI/ProjectFormView.swift"
]
},
{
"query": "servidor HTTP que atende a webapp",
"kind": "conceitual",
"expected": "TigreApp/Web/HTTPServer.swift",
"rank": 1,
"elapsed": 0.2947174580040155,
"bytes": 2780,
"got": [
"TigreApp/Web/HTTPServer.swift",
"TigreApp/Web/WebRunner.swift",
"TigreApp/Web/HTTPRequest.swift",
"TigreApp/Web/TigreWebService.swift",
"TigreApp/Web/WebRunner.swift"
]
},
{
"query": "gravar arquivo em disco de forma atomica",
"kind": "conceitual",
"expected": "TigreFoundation/Files/AtomicFileIO.swift",
"rank": null,
"elapsed": 0.3465013329987414,
"bytes": 0,
"got": []
},
{
"query": "cliente que fala com o Ollama",
"kind": "conceitual",
"expected": "TigreAI/OllamaClient.swift",
"rank": null,
"elapsed": 0.29580008301127236,
"bytes": 0,
"got": []
},
{
"query": "impedir caminho de projeto fora da pasta permitida",
"kind": "conceitual",
"expected": "TigreFoundation/Persistence/ProjectPathSafety.swift",
"rank": null,
"elapsed": 0.39425449998816475,
"bytes": 2922,
"got": [
"TigreAppUI/WizardView.swift",
"TigreAppUI/ProjectFormView.swift",
"TigreVideoAnalysis/Pipeline/FramePerceiver.swift",
"TigreVideoAnalysis/Pipeline/VisionPerceptionPipeline.swift",
"TigreAppUI/WizardView.swift"
]
},
{
"query": "mapear tempo da timeline para o tempo da fonte",
"kind": "conceitual",
"expected": "TigreVideoEditing/TimelineMapper.swift",
"rank": null,
"elapsed": 0.36004945800232235,
"bytes": 0,
"got": []
}
]
}
+284
View File
@@ -0,0 +1,284 @@
{
"summary": {
"label": "baseline-doza",
"mode": "map",
"top_k": 5,
"n": 18,
"recall@5": 0.667,
"top1": 0.556,
"latencia_media_s": 0.474,
"bytes_medios": 645
},
"rows": [
{
"query": "SpineSegment",
"kind": "exato",
"expected": "doza_assist/fcpxml/parser.py",
"rank": 2,
"elapsed": 0.6019748329999857,
"bytes": 659,
"got": [
"doza_assist/fcpxml/timecode.py",
"doza_assist/fcpxml/parser.py",
"scripts/sign-and-notarize.sh",
"doza_assist/fcpxml/writer.py",
"doza_assist/fcpxml/timeline_audio.py"
]
},
{
"query": "OllamaProvider",
"kind": "exato",
"expected": "ai_providers/ollama_provider.py",
"rank": 1,
"elapsed": 0.49162437501945533,
"bytes": 637,
"got": [
"ai_providers/ollama_provider.py",
"ai_providers/ollama_provider.py",
"NOTICES.md",
"launcher.sh",
"ai_analysis.py"
]
},
{
"query": "detect_hardware_tier",
"kind": "exato",
"expected": "model_config.py",
"rank": 2,
"elapsed": 0.4542047090071719,
"bytes": 663,
"got": [
"test_hardware_tier.py",
"model_config.py",
"model_config.py",
"ai_providers/openai_provider.py",
"editorial_dna/storytelling.py"
]
},
{
"query": "snap_framerate",
"kind": "exato",
"expected": "exporters/media_probe.py",
"rank": 1,
"elapsed": 0.478199749981286,
"bytes": 620,
"got": [
"exporters/media_probe.py",
"tests/test_framerate_support.py",
"exporters/media_probe.py",
"app.py",
"fcpxml_export.py"
]
},
{
"query": "whisper_catalog",
"kind": "exato",
"expected": "whisper_catalog.py",
"rank": 1,
"elapsed": 0.47477179099223576,
"bytes": 730,
"got": [
"whisper_catalog.py",
"whisper_catalog.py",
"app.py",
"app.py",
"transcribe.py"
]
},
{
"query": "premiere_xml",
"kind": "exato",
"expected": "exporters/premiere_xml.py",
"rank": 1,
"elapsed": 0.47077204199740663,
"bytes": 701,
"got": [
"exporters/premiere_xml.py",
"exporters/premiere_xml.py",
"tests/test_exporters.py",
"tests/test_exporters.py",
"tests/test_fcpxml.py"
]
},
{
"query": "parakeet worker",
"kind": "exato",
"expected": "parakeet_worker.py",
"rank": 1,
"elapsed": 0.5250667080108542,
"bytes": 671,
"got": [
"parakeet_worker.py",
"parakeet_worker.py",
"transcribe.py",
"setup_assistant.py",
"setup_assistant.py"
]
},
{
"query": "converter segundos em timecode",
"kind": "conceitual",
"expected": "exporters/edl.py",
"rank": null,
"elapsed": 0.507195583981229,
"bytes": 528,
"got": [
"prompts/skills/editar-por-voz/criterios/01-leitura-do-json.md",
"exporters/media_probe.py",
"transcribe.py",
"prompts/skills/editar-por-voz/SKILL.md",
"prompts/skills/editar-por-voz/criterios/10-revisao-humana.md"
]
},
{
"query": "descobrir a resolucao do video com ffprobe",
"kind": "conceitual",
"expected": "exporters/media_probe.py",
"rank": null,
"elapsed": 0.4559026249917224,
"bytes": 620,
"got": [
"prompts/skills/editar-por-voz/criterios/10-revisao-humana.md",
"prompts/skills/editar-por-voz/criterios/04-reanalise-do-material-restante.md",
"prompts/skills/editar-por-voz/SKILL.md",
"prompts/skills/editar-por-voz/SKILL.md",
"prompts/skills/editar-por-voz/criterios/07-ritmo.md"
]
},
{
"query": "detectar quanta memoria RAM a maquina tem",
"kind": "conceitual",
"expected": "model_config.py",
"rank": null,
"elapsed": 0.4457190420071129,
"bytes": 568,
"got": [
"prompts/skills/editar-por-voz/criterios/07-ritmo.md",
"prompts/skills/editar-por-voz/criterios/01-leitura-do-json.md",
"prompts/skills/editar-por-voz/SKILL.md",
"prompts/skills/editar-por-voz/criterios/04-reanalise-do-material-restante.md",
"prompts/skills/editar-por-voz/criterios/04-reanalise-do-material-restante.md"
]
},
{
"query": "escrever o arquivo FCPXML de saida",
"kind": "conceitual",
"expected": "doza_assist/fcpxml/writer.py",
"rank": null,
"elapsed": 0.43871399998897687,
"bytes": 551,
"got": [
"prompts/skills/editar-por-voz/criterios/08-formato-de-saida.md",
"prompts/skills/editar-por-voz/criterios/08-formato-de-saida.md",
"prompts/skills/editar-por-voz/SKILL.md",
"app.py",
"doza_assist/fcpxml/parser.py"
]
},
{
"query": "ler e interpretar um arquivo FCPXML",
"kind": "conceitual",
"expected": "doza_assist/fcpxml/parser.py",
"rank": 1,
"elapsed": 0.44622687497758307,
"bytes": 572,
"got": [
"doza_assist/fcpxml/parser.py",
"app.py",
"doza_assist/fcpxml/parser.py",
"app.py",
"fcpxml_export.py"
]
},
{
"query": "perfis de DNA editorial salvos em disco",
"kind": "conceitual",
"expected": "editorial_dna/profiles.py",
"rank": null,
"elapsed": 0.482757291989401,
"bytes": 713,
"got": [
"prompts/skills/editar-por-voz/criterios/10-revisao-humana.md",
"prompts/skills/editar-por-voz/SKILL.md",
"prompts/skills/editar-por-voz/criterios/10-revisao-humana.md",
"prompts/skills/editar-por-voz/criterios/04-reanalise-do-material-restante.md",
"prompts/skills/editar-por-voz/criterios/04-reanalise-do-material-restante.md"
]
},
{
"query": "instalar e baixar modelos do whisper",
"kind": "conceitual",
"expected": "whisper_catalog.py",
"rank": 1,
"elapsed": 0.47599695800454356,
"bytes": 629,
"got": [
"whisper_catalog.py",
"whisper_catalog.py",
"prompts/skills/editar-por-voz/SKILL.md",
"app.py",
"prompts/skills/editar-por-voz/criterios/09-analise-incompleta.md"
]
},
{
"query": "exportar lista de decisao EDL",
"kind": "conceitual",
"expected": "exporters/edl.py",
"rank": 1,
"elapsed": 0.4421596669999417,
"bytes": 646,
"got": [
"exporters/edl.py",
"exporters/edl.py",
"prompts/skills/editar-por-voz/SKILL.md",
"prompts/skills/editar-por-voz/criterios/08-formato-de-saida.md",
"prompts/skills/editar-por-voz/criterios/04-reanalise-do-material-restante.md"
]
},
{
"query": "transcrever o audio para texto",
"kind": "conceitual",
"expected": "transcribe.py",
"rank": null,
"elapsed": 0.4331624579790514,
"bytes": 615,
"got": [
"prompts/skills/editar-por-voz/SKILL.md",
"prompts/skills/editar-por-voz/criterios/06-texto-corte-marcador.md",
"prompts/skills/editar-por-voz/criterios/06-texto-corte-marcador.md",
"prompts/skills/editar-por-voz/criterios/02-triagem-roteiro-vs-conversa.md",
"prompts/skills/editar-por-voz/SKILL.md"
]
},
{
"query": "analisar video usando o framework Vision",
"kind": "conceitual",
"expected": "video_analysis.py",
"rank": 1,
"elapsed": 0.4570178329886403,
"bytes": 666,
"got": [
"video_analysis.py",
"video_analysis.py",
"app.py",
"prompts/skills/editar-por-voz/SKILL.md",
"prompts/skills/editar-por-voz/criterios/04-reanalise-do-material-restante.md"
]
},
{
"query": "provedor de IA da Anthropic",
"kind": "conceitual",
"expected": "ai_providers/anthropic_provider.py",
"rank": 1,
"elapsed": 0.45130354099092074,
"bytes": 828,
"got": [
"ai_providers/anthropic_provider.py",
"ai_providers/anthropic_provider.py",
"prompts/skills/editar-por-voz/criterios/10-revisao-humana.md",
"tests/test_anthropic_prompt_cache.py",
"tests/test_anthropic_prompt_cache.py"
]
}
]
}
@@ -0,0 +1,266 @@
{
"summary": {
"label": "baseline-doza",
"mode": "snippet",
"top_k": 5,
"n": 18,
"recall@5": 0.0,
"top1": 0.0,
"latencia_media_s": 0.491,
"bytes_medios": 1906
},
"rows": [
{
"query": "FCPXMLValidator",
"kind": "exato",
"expected": "TigreVideoEditing/FCPXMLValidator.swift",
"rank": null,
"elapsed": 1.05223191701225,
"bytes": 2257,
"got": [
"code/doza_assist/fcpxml/__init__.py",
"code/exporters/fcpxml.py",
"code/tests/test_fcpxml_writer.py",
"code/tests/test_fcpxml.py",
"code/transcribe.py"
]
},
{
"query": "SilenceCutPipeline",
"kind": "exato",
"expected": "TigreVideoEditing/SilenceCutPipeline.swift",
"rank": null,
"elapsed": 0.47514604197931476,
"bytes": 2422,
"got": [
"code/tests/test_fcpxml.py",
"code/tests/test_fcpxml.py",
"code/doza_assist/fcpxml/parser.py",
"code/doza_assist/fcpxml/parser.py",
"code/tests/test_whisper_install_guard.py"
]
},
{
"query": "TranscriptIndexBuilder",
"kind": "exato",
"expected": "TigreTranscription/Pipeline/TranscriptIndexBuilder.swift",
"rank": null,
"elapsed": 0.43673787501757033,
"bytes": 2461,
"got": [
"code/ai_analysis.py",
"code/tests/test_chat_layer1_retrieval.py",
"code/exporters/base.py",
"code/tests/test_anthropic_prompt_cache.py",
"code/ai_analysis.py"
]
},
{
"query": "SQLiteProjectStore",
"kind": "exato",
"expected": "TigreFoundation/Persistence/SQLiteProjectStore.swift",
"rank": null,
"elapsed": 0.3961836249800399,
"bytes": 28,
"got": []
},
{
"query": "EditPromptBuilder",
"kind": "exato",
"expected": "TigreAI/EditPromptBuilder.swift",
"rank": null,
"elapsed": 0.4493663749890402,
"bytes": 2242,
"got": [
"code/README.md",
"code/tests/test_fcpxml.py",
"code/setup_runner.sh",
"code/setup_assistant.py",
"code/app.py"
]
},
{
"query": "UniformFrameScheduler",
"kind": "exato",
"expected": "TigreVideoAnalysis/Pipeline/UniformFrameScheduler.swift",
"rank": null,
"elapsed": 0.4161145419930108,
"bytes": 2364,
"got": [
"code/doza_assist/fcpxml/__init__.py",
"code/tests/test_fcpxml.py",
"code/app.py",
"code/tests/test_fcpxml_writer.py",
"code/ai_analysis.py"
]
},
{
"query": "TemplateRenderer",
"kind": "exato",
"expected": "TigreFoundation/Web/TemplateRenderer.swift",
"rank": null,
"elapsed": 0.4430151670239866,
"bytes": 2323,
"got": [
"code/exporters/__init__.py",
"code/editorial_dna/analysis.py",
"code/fcpxml_export.py",
"code/tests/test_exporters.py",
"code/exporters/edl.py"
]
},
{
"query": "como o wizard decide qual e o proximo passo",
"kind": "conceitual",
"expected": "TigreAppUI/WizardStep.swift",
"rank": null,
"elapsed": 0.4231184579839464,
"bytes": 471,
"got": [
"code/install.sh"
]
},
{
"query": "extrair o audio de um video com ffmpeg",
"kind": "conceitual",
"expected": "TigreTranscription/Pipeline/FFmpegAudioExtractor.swift",
"rank": null,
"elapsed": 0.48013929100125097,
"bytes": 2334,
"got": [
"code/tests/test_fcpxml_timeline_audio.py",
"code/transcribe.py",
"code/transcribe.py",
"code/tests/test_issue39_camera_media.py",
"code/exporters/media_probe.py"
]
},
{
"query": "detectar trechos de silencio no audio",
"kind": "conceitual",
"expected": "TigreVideoEditing/SilenceAnalyzer.swift",
"rank": null,
"elapsed": 0.4846114999963902,
"bytes": 2384,
"got": [
"code/editorial_dna/transcript_analyzer.py",
"code/scripts/sign-and-notarize.sh",
"code/tests/test_fcpxml.py",
"code/app.py",
"code/editorial_dna/transcript_analyzer.py"
]
},
{
"query": "baixar o modelo do whisper",
"kind": "conceitual",
"expected": "TigreTranscription/Pipeline/WhisperModelDownloader.swift",
"rank": null,
"elapsed": 0.40840750001370907,
"bytes": 2408,
"got": [
"code/whisper_catalog.py",
"code/app.py",
"code/whisper_catalog.py",
"code/transcribe.py",
"code/app.py"
]
},
{
"query": "escrever o arquivo final de FCPXML",
"kind": "conceitual",
"expected": "TigreVideoEditing/FCPXMLWriter.swift",
"rank": null,
"elapsed": 0.4037720419873949,
"bytes": 471,
"got": [
"code/install.sh"
]
},
{
"query": "detectar rostos nos quadros do video",
"kind": "conceitual",
"expected": "TigreVideoAnalysis/Pipeline/FacePerceiver.swift",
"rank": null,
"elapsed": 0.4111621659831144,
"bytes": 471,
"got": [
"code/install.sh"
]
},
{
"query": "servidor HTTP que atende a webapp",
"kind": "conceitual",
"expected": "TigreApp/Web/HTTPServer.swift",
"rank": null,
"elapsed": 0.47790624998742715,
"bytes": 2411,
"got": [
"code/model_config.py",
"code/app.py",
"code/tests/test_issue39_camera_media.py",
"code/doza_assist/fcpxml/timeline_audio.py",
"code/setup_assistant.py"
]
},
{
"query": "gravar arquivo em disco de forma atomica",
"kind": "conceitual",
"expected": "TigreFoundation/Files/AtomicFileIO.swift",
"rank": null,
"elapsed": 0.43502945799264126,
"bytes": 2254,
"got": [
"code/requirements.txt",
"code/app.py",
"code/tests/test_fcpxml_timeline_audio.py",
"code/transcribe.py",
"code/CLA.md"
]
},
{
"query": "cliente que fala com o Ollama",
"kind": "conceitual",
"expected": "TigreAI/OllamaClient.swift",
"rank": null,
"elapsed": 0.5566641250043176,
"bytes": 2401,
"got": [
"code/app.py",
"code/setup_assistant.py",
"code/whisper_catalog.py",
"code/doza_assist/fcpxml/__init__.py",
"code/ai_analysis.py"
]
},
{
"query": "impedir caminho de projeto fora da pasta permitida",
"kind": "conceitual",
"expected": "TigreFoundation/Persistence/ProjectPathSafety.swift",
"rank": null,
"elapsed": 0.6178763750067446,
"bytes": 2154,
"got": [
"code/CLA.md",
"code/README.md",
"code/requirements.txt",
"code/CLA.md",
"code/README.md"
]
},
{
"query": "mapear tempo da timeline para o tempo da fonte",
"kind": "conceitual",
"expected": "TigreVideoEditing/TimelineMapper.swift",
"rank": null,
"elapsed": 0.47090933300205506,
"bytes": 2448,
"got": [
"code/editorial_dna/snapshots.py",
"code/video_analysis.py",
"code/app.py",
"code/NOTICES.md",
"code/NOTICES.md"
]
}
]
}
+284
View File
@@ -0,0 +1,284 @@
{
"summary": {
"label": "otimizado-doza",
"mode": "map",
"top_k": 5,
"n": 18,
"recall@5": 0.722,
"top1": 0.611,
"latencia_media_s": 0.675,
"bytes_medios": 692
},
"rows": [
{
"query": "SpineSegment",
"kind": "exato",
"expected": "doza_assist/fcpxml/parser.py",
"rank": 3,
"elapsed": 0.699377083015861,
"bytes": 738,
"got": [
"editorial_dna/summarizer.py",
"doza_assist/fcpxml/timecode.py",
"doza_assist/fcpxml/parser.py",
"prompts/skills/editar-por-voz/criterios/04-reanalise-do-material-restante.md",
"tests/test_story_builder.py"
]
},
{
"query": "OllamaProvider",
"kind": "exato",
"expected": "ai_providers/ollama_provider.py",
"rank": 1,
"elapsed": 0.6421885000017937,
"bytes": 767,
"got": [
"ai_providers/ollama_provider.py",
"ai_providers/ollama_provider.py",
"doza_assist/fcpxml/parser.py",
"editorial_dna/fcpxml_ingest.py",
"tests/test_fcpxml.py"
]
},
{
"query": "detect_hardware_tier",
"kind": "exato",
"expected": "model_config.py",
"rank": 1,
"elapsed": 0.6680177910020575,
"bytes": 694,
"got": [
"model_config.py",
"test_hardware_tier.py",
"tests/test_ai_model_status_project_scope.py",
"tests/test_analysis_cap.py",
"editorial_dna/classifier.py"
]
},
{
"query": "snap_framerate",
"kind": "exato",
"expected": "exporters/media_probe.py",
"rank": 2,
"elapsed": 0.6498886670160573,
"bytes": 677,
"got": [
"tests/test_framerate_support.py",
"exporters/media_probe.py",
"app.py",
"doza_assist/fcpxml/parser.py",
"exporters/media_probe.py"
]
},
{
"query": "whisper_catalog",
"kind": "exato",
"expected": "whisper_catalog.py",
"rank": 1,
"elapsed": 0.6533211249916349,
"bytes": 779,
"got": [
"whisper_catalog.py",
"tests/test_whisper_install_guard.py",
"whisper_catalog.py",
"app.py",
"doza_assist/retrieval.py"
]
},
{
"query": "premiere_xml",
"kind": "exato",
"expected": "exporters/premiere_xml.py",
"rank": 1,
"elapsed": 0.6422539160121232,
"bytes": 721,
"got": [
"exporters/premiere_xml.py",
"exporters/premiere_xml.py",
"doza_assist/fcpxml/timeline_audio.py",
"tests/test_exporters.py",
"tests/test_exporters.py"
]
},
{
"query": "parakeet worker",
"kind": "exato",
"expected": "parakeet_worker.py",
"rank": 1,
"elapsed": 0.67050391601515,
"bytes": 685,
"got": [
"parakeet_worker.py",
"setup_assistant.py",
"transcribe.py",
"parakeet_worker.py",
"ai_providers/anthropic_provider.py"
]
},
{
"query": "converter segundos em timecode",
"kind": "conceitual",
"expected": "exporters/edl.py",
"rank": null,
"elapsed": 0.6502645000000484,
"bytes": 649,
"got": [
"doza_assist/fcpxml/timecode.py",
"prompts/skills/editar-por-voz/criterios/10-revisao-humana.md",
"tests/test_fcpxml_timecode.py",
"prompts/skills/editar-por-voz/criterios/01-leitura-do-json.md",
"prompts/skills/editar-por-voz/criterios/03-escolha-da-melhor-tomada.md"
]
},
{
"query": "descobrir a resolucao do video com ffprobe",
"kind": "conceitual",
"expected": "exporters/media_probe.py",
"rank": null,
"elapsed": 0.6691363749851007,
"bytes": 725,
"got": [
"prompts/skills/editar-por-voz/criterios/10-revisao-humana.md",
"prompts/skills/editar-por-voz/criterios/04-reanalise-do-material-restante.md",
"doza_assist/fcpxml/timeline_audio.py",
"prompts/skills/editar-por-voz/criterios/01-leitura-do-json.md",
"doza_assist/fcpxml/parser.py"
]
},
{
"query": "detectar quanta memoria RAM a maquina tem",
"kind": "conceitual",
"expected": "model_config.py",
"rank": null,
"elapsed": 0.679498915997101,
"bytes": 624,
"got": [
"prompts/skills/editar-por-voz/criterios/01-leitura-do-json.md",
"prompts/skills/editar-por-voz/criterios/04-reanalise-do-material-restante.md",
"prompts/skills/editar-por-voz/criterios/03-escolha-da-melhor-tomada.md",
"prompts/skills/editar-por-voz/criterios/10-revisao-humana.md",
"prompts/skills/editar-por-voz/criterios/07-ritmo.md"
]
},
{
"query": "escrever o arquivo FCPXML de saida",
"kind": "conceitual",
"expected": "doza_assist/fcpxml/writer.py",
"rank": null,
"elapsed": 0.6904783749778289,
"bytes": 637,
"got": [
"prompts/skills/editar-por-voz/criterios/08-formato-de-saida.md",
"doza_assist/fcpxml/parser.py",
"fcpxml_export.py",
"prompts/skills/editar-por-voz/criterios/03-escolha-da-melhor-tomada.md",
"tests/test_fcpxml.py"
]
},
{
"query": "ler e interpretar um arquivo FCPXML",
"kind": "conceitual",
"expected": "doza_assist/fcpxml/parser.py",
"rank": 1,
"elapsed": 0.7000019999977667,
"bytes": 616,
"got": [
"doza_assist/fcpxml/parser.py",
"editorial_dna/fcpxml_ingest.py",
"fcpxml_export.py",
"doza_assist/fcpxml/timecode.py",
"tests/test_fcpxml_ingest.py"
]
},
{
"query": "perfis de DNA editorial salvos em disco",
"kind": "conceitual",
"expected": "editorial_dna/profiles.py",
"rank": 1,
"elapsed": 0.6731580840132665,
"bytes": 625,
"got": [
"editorial_dna/profiles.py",
"prompts/skills/editar-por-voz/criterios/10-revisao-humana.md",
"editorial_dna/snapshots.py",
"editorial_dna/injector.py",
"editorial_dna/storage.py"
]
},
{
"query": "instalar e baixar modelos do whisper",
"kind": "conceitual",
"expected": "whisper_catalog.py",
"rank": 1,
"elapsed": 0.712842458015075,
"bytes": 698,
"got": [
"whisper_catalog.py",
"prompts/skills/editar-por-voz/criterios/10-revisao-humana.md",
"tests/test_whisper_install_guard.py",
"prompts/skills/editar-por-voz/criterios/03-escolha-da-melhor-tomada.md",
"prompts/skills/editar-por-voz/criterios/01-leitura-do-json.md"
]
},
{
"query": "exportar lista de decisao EDL",
"kind": "conceitual",
"expected": "exporters/edl.py",
"rank": 1,
"elapsed": 0.6753168329887558,
"bytes": 631,
"got": [
"exporters/edl.py",
"prompts/skills/editar-por-voz/criterios/08-formato-de-saida.md",
"prompts/skills/editar-por-voz/criterios/09-analise-incompleta.md",
"prompts/skills/editar-por-voz/criterios/10-revisao-humana.md",
"exporters/__init__.py"
]
},
{
"query": "transcrever o audio para texto",
"kind": "conceitual",
"expected": "transcribe.py",
"rank": null,
"elapsed": 0.6860910000104923,
"bytes": 753,
"got": [
"prompts/skills/editar-por-voz/criterios/06-texto-corte-marcador.md",
"scripts/test_skill_editar_por_voz.py",
"prompts/skills/editar-por-voz/criterios/03-escolha-da-melhor-tomada.md",
"prompts/skills/editar-por-voz/criterios/02-triagem-roteiro-vs-conversa.md",
"prompts/skills/editar-por-voz/criterios/10-revisao-humana.md"
]
},
{
"query": "analisar video usando o framework Vision",
"kind": "conceitual",
"expected": "video_analysis.py",
"rank": 1,
"elapsed": 0.647432875004597,
"bytes": 661,
"got": [
"video_analysis.py",
"app.py",
"prompts/skills/editar-por-voz/criterios/09-analise-incompleta.md",
"scripts/test_skill_editar_por_voz.py",
"prompts/skills/editar-por-voz/criterios/10-revisao-humana.md"
]
},
{
"query": "provedor de IA da Anthropic",
"kind": "conceitual",
"expected": "ai_providers/anthropic_provider.py",
"rank": 1,
"elapsed": 0.7330960000108462,
"bytes": 774,
"got": [
"ai_providers/anthropic_provider.py",
"tests/test_anthropic_prompt_cache.py",
"prompts/skills/editar-por-voz/criterios/10-revisao-humana.md",
"ai_providers/anthropic_provider.py",
"prompts/skills/editar-por-voz/criterios/01-leitura-do-json.md"
]
}
]
}
+284
View File
@@ -0,0 +1,284 @@
{
"summary": {
"label": "otimizado",
"mode": "map",
"top_k": 5,
"n": 18,
"recall@5": 0.667,
"top1": 0.278,
"latencia_media_s": 1.324,
"bytes_medios": 633
},
"rows": [
{
"query": "OAuthResourceServer",
"kind": "exato",
"expected": "src/oauth-resource-server.ts",
"rank": 1,
"elapsed": 1.8486205840017647,
"bytes": 738,
"got": [
"src/oauth-resource-server.ts",
"tests/oauth-resource-server.test.ts",
"src/resources/live-context-resources.ts",
"tests/oauth-resource-server.test.ts",
"docs/recommendations/2026-08-19-round-3/47-resource-uri-policy.md"
]
},
{
"query": "UxpWebSocketBridge",
"kind": "exato",
"expected": "src/bridge/uxp-websocket-bridge.ts",
"rank": 1,
"elapsed": 1.0479179159956402,
"bytes": 572,
"got": [
"src/bridge/uxp-websocket-bridge.ts",
"src/tools/uxp.ts",
"src/bridge/uxp-websocket-bridge.ts",
"docs/recommendations/2026-08-18/11-uxp-backpressure.md",
"uxp-plugin/README.md"
]
},
{
"query": "ProjectContextRepository",
"kind": "exato",
"expected": "src/context/project-context-store.ts",
"rank": 2,
"elapsed": 1.0680564579961356,
"bytes": 931,
"got": [
"src/tools/project-context.ts",
"src/context/project-context-store.ts",
"src/ai/editorial-context-pack.ts",
"src/context/project-context-store.ts",
"src/tools/editorial-context-pack.ts"
]
},
{
"query": "applyDoctorRepairPlan",
"kind": "exato",
"expected": "src/doctor-repairs.ts",
"rank": 1,
"elapsed": 1.0746368340041954,
"bytes": 845,
"got": [
"src/doctor-repairs.ts",
"src/diagnostics.ts",
"src/doctor-repairs.ts",
"docs/recommendations/2026-08-19-round-3/43-mrtr-roots-boundary.md",
"docs/industry/security-and-design-partner-pilot.md"
]
},
{
"query": "buildContentSecurityPolicy",
"kind": "exato",
"expected": "src/http-security.ts",
"rank": 1,
"elapsed": 1.1980962499947054,
"bytes": 657,
"got": [
"src/http-security.ts",
"src/bridge/script-builder.ts",
"src/resources/live-context-resources.ts",
"tests/security-capabilities.test.ts",
"scripts/build-chat-dmg.sh"
]
},
{
"query": "planDerivedSilenceRemoval",
"kind": "exato",
"expected": "src/tools/silence-removal.ts",
"rank": null,
"elapsed": 2.106093750000582,
"bytes": 707,
"got": [
"src/tools/editorial-plans.ts",
"product-growth-runs/premiere-pro-mcp/2026-08-22/06-paid-ads.md",
"product-growth-runs/premiere-pro-mcp/2026-08-22/00-executive-summary.md",
"product-growth-runs/premiere-pro-mcp/2026-08-22/04-design-creative-brief.md",
"docs/recommendations/2026-08-18/04-operation-scheduler.md"
]
},
{
"query": "buildCaptionTimingPlan",
"kind": "exato",
"expected": "src/ai/caption-timing.ts",
"rank": 2,
"elapsed": 1.1320488339988515,
"bytes": 583,
"got": [
"src/bridge/script-builder.ts",
"src/ai/caption-timing.ts",
"tests/docker-build-context.test.ts",
"product-growth-runs/premiere-pro-mcp/2026-08-22/00-executive-summary.md",
"landing/app/project-intake/project-intake-template-builder.tsx"
]
},
{
"query": "emitAudit",
"kind": "exato",
"expected": "src/security/audit.ts",
"rank": 2,
"elapsed": 1.0001867090031737,
"bytes": 647,
"got": [
"docs/mogrt-authoring.md",
"src/security/audit.ts",
"tests/tools/detect-silence.test.ts",
"src/tools/audio.ts",
"docs/recommendations/2026-08-18-round-2/40-export-reconciliation.md"
]
},
{
"query": "cabecalhos de seguranca HTTP e CSP",
"kind": "conceitual",
"expected": "src/http-security.ts",
"rank": 3,
"elapsed": 1.1984910419996595,
"bytes": 532,
"got": [
"docs/panel-ui-guidelines.md",
"cep-plugin/main.js",
"src/http-security.ts",
"cep-plugin/CSInterface.js",
"src/http-admission.ts"
]
},
{
"query": "registrar evento de telemetria de ativacao",
"kind": "conceitual",
"expected": "src/telemetry.ts",
"rank": null,
"elapsed": 1.2329936659953091,
"bytes": 633,
"got": [
"docs/panel-ui-guidelines.md",
"docs/marketing/mcp-registry-readiness.md",
"landing/lib/onboarding-events.ts",
"docs/recommendations/2026-08-18-round-2/30-host-capability-attestation.md",
"src/oauth-resource-server.ts"
]
},
{
"query": "ponte websocket com o plugin UXP",
"kind": "conceitual",
"expected": "src/bridge/uxp-websocket-bridge.ts",
"rank": 1,
"elapsed": 1.3489117919962155,
"bytes": 436,
"got": [
"src/bridge/uxp-websocket-bridge.ts",
"docs/panel-ui-guidelines.md",
"docs/recommendations/2026-08-18/13-uxp-heartbeat.md",
"uxp-plugin/manifest.json",
"src/tools/uxp.ts"
]
},
{
"query": "interpretar arquivo de legenda SRT ou VTT",
"kind": "conceitual",
"expected": "src/ai/caption-timing.ts",
"rank": null,
"elapsed": 1.224028584001644,
"bytes": 471,
"got": [
"src/tools/audio.ts",
"docs/quickstart/es.md",
"docs/panel-ui-guidelines.md",
"tests/uxp/timeline-source-label-workflows.test.ts",
"tests/uxp/source-media-provenance-workflows.test.ts"
]
},
{
"query": "plano de remocao de silencio derivado da transcricao",
"kind": "conceitual",
"expected": "src/tools/silence-removal.ts",
"rank": null,
"elapsed": 1.3952149579999968,
"bytes": 506,
"got": [
"src/tools/edit-plans.ts",
"src/tools/editorial-plans.ts",
"docs/quickstart/es.md",
"docs/panel-ui-guidelines.md",
"src/tools/transcript-edits.ts"
]
},
{
"query": "armazenar contexto do projeto em disco",
"kind": "conceitual",
"expected": "src/context/project-context-store.ts",
"rank": null,
"elapsed": 1.2288835830040625,
"bytes": 700,
"got": [
"docs/quickstart/es.md",
"docs/panel-ui-guidelines.md",
"src/tools/project-context.ts",
"docs/recommendations/2026-08-18/16-context-delta-capture.md",
"docs/recommendations/2026-08-19-round-3/44-resource-context-annotations.md"
]
},
{
"query": "checagem e reparo automatico de instalacao (doctor)",
"kind": "conceitual",
"expected": "src/doctor-repairs.ts",
"rank": 4,
"elapsed": 1.3927548749998095,
"bytes": 562,
"got": [
"docs/panel-ui-guidelines.md",
"docs/doctor-repair-plans.md",
"docs/quickstart/es.md",
"src/doctor-repairs.ts",
"docs/recommendations/2026-08-19-round-3/48-c2pa-inspection-lab.md"
]
},
{
"query": "relatorio de intake do projeto",
"kind": "conceitual",
"expected": "src/intake/project-intake.ts",
"rank": 3,
"elapsed": 1.1840198330028215,
"bytes": 585,
"got": [
"docs/industry/project-intake-workflow.md",
"landing/app/project-intake/page.tsx",
"src/intake/project-intake.ts",
"tests/project-intake.test.ts",
"docs/project-intake-host-report.schema.json"
]
},
{
"query": "servidor OAuth para autenticacao de recursos",
"kind": "conceitual",
"expected": "src/oauth-resource-server.ts",
"rank": 2,
"elapsed": 1.3320905420041527,
"bytes": 647,
"got": [
"tests/oauth-resource-server.test.ts",
"src/oauth-resource-server.ts",
"docs/panel-ui-guidelines.md",
"src/http-admission.ts",
"docs/recommendations/2026-08-18/03-auth-scoped-throttling.md"
]
},
{
"query": "registrar operacao de auditoria de seguranca",
"kind": "conceitual",
"expected": "src/security/audit.ts",
"rank": null,
"elapsed": 1.8265332500013756,
"bytes": 636,
"got": [
"docs/panel-ui-guidelines.md",
"docs/recommendations/2026-08-19-round-3/48-c2pa-inspection-lab.md",
"src/oauth-resource-server.ts",
"security_best_practices_report.md",
"docs/recommendations/2026-08-18/19-object-mask-audit.md"
]
}
]
}
+206
View File
@@ -0,0 +1,206 @@
"""Benchmark da RAG: mede recall@k e custo de tokens da busca.
Roda um conjunto dourado de consultas contra o índice e reporta, para cada
uma, se o arquivo esperado apareceu no top-k e em que posição. No fim mostra
recall@k agregado, latência média e o tamanho médio da saída — as três coisas
que o RAG precisa otimizar (achar o arquivo certo, rápido, gastando pouco
token).
Uso:
rag/bench.sh # roda tudo
rag/bench.sh --mode snippet # mede o custo de outro modo de saída
rag/bench.sh --baseline # usa a busca densa pura (pré-otimização)
O resultado é impresso e também salvo em rag/bench-results/<rotulo>.json,
para comparar antes/depois.
"""
import argparse
import json
import os
import statistics
import sys
import time
RAG_DIR = os.path.dirname(os.path.abspath(__file__))
if RAG_DIR not in sys.path:
sys.path.insert(0, RAG_DIR)
# Carrega o env ANTES de escolher o conjunto dourado. Sem isso o schema seria
# lido do ambiente do processo, onde não está — quem o define é o arquivo
# .env, e o search.py só o carrega quando é importado, tarde demais.
from dotenv import load_dotenv # noqa: E402
load_dotenv(os.environ.get("RAG_ENV_FILE", os.path.join(RAG_DIR, ".env")))
# Conjuntos dourados, um por sistema indexado: (consulta, arquivo esperado,
# categoria).
# - "exato": o usuário sabe o nome do tipo/função e quer o arquivo dele.
# - "conceitual": descreve comportamento, sem citar identificador.
# - "modulo": pergunta ampla sobre uma área do sistema.
GOLDEN_TIGRE = [
# --- nome exato de tipo (onde a busca densa pura falha) ---
("FCPXMLValidator", "TigreVideoEditing/FCPXMLValidator.swift", "exato"),
("SilenceCutPipeline", "TigreVideoEditing/SilenceCutPipeline.swift", "exato"),
("TranscriptIndexBuilder", "TigreTranscription/Pipeline/TranscriptIndexBuilder.swift", "exato"),
("SQLiteProjectStore", "TigreFoundation/Persistence/SQLiteProjectStore.swift", "exato"),
("EditPromptBuilder", "TigreAI/EditPromptBuilder.swift", "exato"),
("UniformFrameScheduler", "TigreVideoAnalysis/Pipeline/UniformFrameScheduler.swift", "exato"),
("TemplateRenderer", "TigreFoundation/Web/TemplateRenderer.swift", "exato"),
# --- conceitual: descreve o comportamento ---
("como o wizard decide qual e o proximo passo", "TigreAppUI/WizardStep.swift", "conceitual"),
("extrair o audio de um video com ffmpeg", "TigreTranscription/Pipeline/FFmpegAudioExtractor.swift", "conceitual"),
("detectar trechos de silencio no audio", "TigreVideoEditing/SilenceAnalyzer.swift", "conceitual"),
("baixar o modelo do whisper", "TigreTranscription/Pipeline/WhisperModelDownloader.swift", "conceitual"),
("escrever o arquivo final de FCPXML", "TigreVideoEditing/FCPXMLWriter.swift", "conceitual"),
("detectar rostos nos quadros do video", "TigreVideoAnalysis/Pipeline/FacePerceiver.swift", "conceitual"),
("servidor HTTP que atende a webapp", "TigreApp/Web/HTTPServer.swift", "conceitual"),
("gravar arquivo em disco de forma atomica", "TigreFoundation/Files/AtomicFileIO.swift", "conceitual"),
("cliente que fala com o Ollama", "TigreAI/OllamaClient.swift", "conceitual"),
("impedir caminho de projeto fora da pasta permitida", "TigreFoundation/Persistence/ProjectPathSafety.swift", "conceitual"),
("mapear tempo da timeline para o tempo da fonte", "TigreVideoEditing/TimelineMapper.swift", "conceitual"),
]
# O legado é Python e NÃO segue "uma classe por arquivo": há módulos de 5000
# linhas com dezenas de funções soltas. O conjunto reflete isso — as
# consultas exatas citam função, não só classe.
GOLDEN_DOZA = [
("SpineSegment", "doza_assist/fcpxml/parser.py", "exato"),
("OllamaProvider", "ai_providers/ollama_provider.py", "exato"),
("detect_hardware_tier", "model_config.py", "exato"),
("snap_framerate", "exporters/media_probe.py", "exato"),
("whisper_catalog", "whisper_catalog.py", "exato"),
("premiere_xml", "exporters/premiere_xml.py", "exato"),
("parakeet worker", "parakeet_worker.py", "exato"),
("converter segundos em timecode", "exporters/edl.py", "conceitual"),
("descobrir a resolucao do video com ffprobe", "exporters/media_probe.py", "conceitual"),
("detectar quanta memoria RAM a maquina tem", "model_config.py", "conceitual"),
("escrever o arquivo FCPXML de saida", "doza_assist/fcpxml/writer.py", "conceitual"),
("ler e interpretar um arquivo FCPXML", "doza_assist/fcpxml/parser.py", "conceitual"),
("perfis de DNA editorial salvos em disco", "editorial_dna/profiles.py", "conceitual"),
("instalar e baixar modelos do whisper", "whisper_catalog.py", "conceitual"),
("exportar lista de decisao EDL", "exporters/edl.py", "conceitual"),
("transcrever o audio para texto", "transcribe.py", "conceitual"),
("analisar video usando o framework Vision", "video_analysis.py", "conceitual"),
("provedor de IA da Anthropic", "ai_providers/anthropic_provider.py", "conceitual"),
]
# Projeto Jhonny: MCP em TypeScript para controlar o Premiere Pro (code/src,
# code/tools). Consultas exatas citam a classe/funcao exportada; conceituais
# descrevem o comportamento sem citar identificador.
GOLDEN_JHONNY = [
("OAuthResourceServer", "src/oauth-resource-server.ts", "exato"),
("UxpWebSocketBridge", "src/bridge/uxp-websocket-bridge.ts", "exato"),
("ProjectContextRepository", "src/context/project-context-store.ts", "exato"),
("applyDoctorRepairPlan", "src/doctor-repairs.ts", "exato"),
("buildContentSecurityPolicy", "src/http-security.ts", "exato"),
("planDerivedSilenceRemoval", "src/tools/silence-removal.ts", "exato"),
("buildCaptionTimingPlan", "src/ai/caption-timing.ts", "exato"),
("emitAudit", "src/security/audit.ts", "exato"),
("cabecalhos de seguranca HTTP e CSP", "src/http-security.ts", "conceitual"),
("registrar evento de telemetria de ativacao", "src/telemetry.ts", "conceitual"),
("ponte websocket com o plugin UXP", "src/bridge/uxp-websocket-bridge.ts", "conceitual"),
("interpretar arquivo de legenda SRT ou VTT", "src/ai/caption-timing.ts", "conceitual"),
("plano de remocao de silencio derivado da transcricao", "src/tools/silence-removal.ts", "conceitual"),
("armazenar contexto do projeto em disco", "src/context/project-context-store.ts", "conceitual"),
("checagem e reparo automatico de instalacao (doctor)", "src/doctor-repairs.ts", "conceitual"),
("relatorio de intake do projeto", "src/intake/project-intake.ts", "conceitual"),
("servidor OAuth para autenticacao de recursos", "src/oauth-resource-server.ts", "conceitual"),
("registrar operacao de auditoria de seguranca", "src/security/audit.ts", "conceitual"),
]
# O conjunto e o prefixo de caminho seguem o schema apontado pelo env, para o
# mesmo bench servir os tres bancos sem flag extra.
_SCHEMA = os.environ.get("RAG_DB_SCHEMA", "tigre")
if _SCHEMA == "doza":
GOLDEN, PREFIX = GOLDEN_DOZA, "code/"
elif _SCHEMA == "jhonny-rag":
GOLDEN, PREFIX = GOLDEN_JHONNY, "code/"
else:
GOLDEN, PREFIX = GOLDEN_TIGRE, "codeclass/Sources/"
def _run(top_k, use_baseline, mode):
from search import rag_search
rows = []
for query, expected, kind in GOLDEN:
expected_path = PREFIX + expected
t0 = time.perf_counter()
if use_baseline:
results = rag_search(query, top_k=top_k, dense_only=True)
else:
results = rag_search(query, top_k=top_k)
elapsed = time.perf_counter() - t0
paths = [r["file_path"] for r in results]
rank = paths.index(expected_path) + 1 if expected_path in paths else None
rows.append({
"query": query,
"kind": kind,
"expected": expected,
"rank": rank,
"elapsed": elapsed,
"bytes": _render_bytes(results, mode),
"got": [p[len(PREFIX):] if p.startswith(PREFIX) else p for p in paths],
})
return rows
def _render_bytes(results, mode):
"""Tamanho da saída que o agente realmente receberia neste modo."""
from search import format_results
return len(format_results(results, mode=mode))
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--top-k", type=int, default=5)
ap.add_argument("--mode", default="map", help="map | snippet | full")
ap.add_argument("--baseline", action="store_true",
help="busca densa pura, sem fusao lexica (estado pre-otimizacao)")
ap.add_argument("--label", default=None, help="nome do arquivo de resultado")
args = ap.parse_args()
rows = _run(args.top_k, args.baseline, args.mode)
hits = [r for r in rows if r["rank"] is not None]
print(f"\n{'ok':>3} {'#':>2} {'cat':<11} consulta")
print("-" * 72)
for r in rows:
mark = "OK " if r["rank"] else "-- "
pos = str(r["rank"]) if r["rank"] else "x"
print(f"{mark:>3} {pos:>2} {r['kind']:<11} {r['query'][:48]}")
if not r["rank"]:
print(f"{'':>18} esperado: {r['expected']}")
print(f"{'':>18} veio: {', '.join(r['got'][:3])}")
recall = len(hits) / len(rows)
top1 = sum(1 for r in hits if r["rank"] == 1) / len(rows)
summary = {
"label": args.label or ("baseline" if args.baseline else "otimizado"),
"mode": args.mode,
"top_k": args.top_k,
"n": len(rows),
f"recall@{args.top_k}": round(recall, 3),
"top1": round(top1, 3),
"latencia_media_s": round(statistics.mean(r["elapsed"] for r in rows), 3),
"bytes_medios": round(statistics.mean(r["bytes"] for r in rows)),
}
print("-" * 72)
for k, v in summary.items():
print(f" {k:<18} {v}")
out_dir = os.path.join(RAG_DIR, "bench-results")
os.makedirs(out_dir, exist_ok=True)
out = os.path.join(out_dir, f"{summary['label']}-{args.mode}.json")
with open(out, "w", encoding="utf-8") as f:
json.dump({"summary": summary, "rows": rows}, f, indent=2, ensure_ascii=False)
print(f"\n -> {os.path.relpath(out, os.path.dirname(RAG_DIR))}")
if __name__ == "__main__":
main()
Executable
+10
View File
@@ -0,0 +1,10 @@
#!/bin/bash
# Benchmark da RAG do Tigre. Ver rag/bench.py.
# Uso: rag/bench.sh [--baseline] [--mode map|snippet|full] [--label nome]
set -e
RAG_DIR="$(cd "$(dirname "$0")" && pwd)"
"$RAG_DIR/ensure_tunnel.sh" >&2
PY="${RAG_DIR}/.venv/bin/python3"
[ -x "$PY" ] || PY="$(command -v python3)"
export RAG_ENV_FILE="$RAG_DIR/tigre.env"
exec "$PY" "$RAG_DIR/bench.py" "$@"
+9
View File
@@ -0,0 +1,9 @@
#!/bin/bash
# Benchmark da RAG do sistema legado (banco rag_doza). Ver rag/bench.py.
set -e
RAG_DIR="$(cd "$(dirname "$0")" && pwd)"
"$RAG_DIR/ensure_tunnel.sh" >&2
PY="${RAG_DIR}/.venv/bin/python3"
[ -x "$PY" ] || PY="$(command -v python3)"
export RAG_ENV_FILE="$RAG_DIR/.env"
exec "$PY" "$RAG_DIR/bench.py" "$@"
+9
View File
@@ -0,0 +1,9 @@
#!/bin/bash
# Benchmark da RAG do projeto Jhonny (banco jhonny-rag). Ver rag/bench.py.
set -e
RAG_DIR="$(cd "$(dirname "$0")" && pwd)"
"$RAG_DIR/ensure_tunnel.sh" >&2
PY="${RAG_DIR}/.venv/bin/python3"
[ -x "$PY" ] || PY="$(command -v python3)"
export RAG_ENV_FILE="$RAG_DIR/jhonny.env"
exec "$PY" "$RAG_DIR/bench.py" "$@"
+36
View File
@@ -0,0 +1,36 @@
#!/bin/bash
# Rode este script UMA VEZ, no seu terminal (fora do Claude Code), para criar
# uma credencial dedicada de indexação e já preencher rag/.env com ela.
# Nunca expõe nem precisa da senha do rag_admin.
set -e
D="$(cd "$(dirname "$0")" && pwd)"
PW="$(python3 -c 'import secrets; print(secrets.token_urlsafe(24))')"
HOST="root@179.197.228.240"
echo "Criando role 'rag_doza_indexer' (escopo: schema doza, sem tocar em rag_admin)..."
ssh "$HOST" "docker exec -i rag-hub-db psql -U rag_admin -d rag_doza -v ON_ERROR_STOP=1" <<SQL
DO \$\$ BEGIN
IF NOT EXISTS (SELECT FROM pg_roles WHERE rolname='rag_doza_indexer') THEN
CREATE ROLE rag_doza_indexer LOGIN PASSWORD '$PW';
ELSE
ALTER ROLE rag_doza_indexer PASSWORD '$PW';
END IF;
END \$\$;
GRANT USAGE, CREATE ON SCHEMA doza TO rag_doza_indexer;
GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA doza TO rag_doza_indexer;
ALTER DEFAULT PRIVILEGES IN SCHEMA doza GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO rag_doza_indexer;
GRANT USAGE ON ALL SEQUENCES IN SCHEMA doza TO rag_doza_indexer;
ALTER DEFAULT PRIVILEGES IN SCHEMA doza GRANT USAGE ON SEQUENCES TO rag_doza_indexer;
SQL
cat > "$D/.env" <<EOF
RAG_DB_HOST=127.0.0.1
RAG_DB_PORT=55435
RAG_DB_NAME=rag_doza
RAG_DB_USER=rag_doza_indexer
RAG_DB_PASSWORD=$PW
RAG_DB_SCHEMA=doza
EOF
echo "OK: rag/.env preenchido com a credencial dedicada rag_doza_indexer."
+37
View File
@@ -0,0 +1,37 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>com.doza.rag-tunnel</string>
<key>ProgramArguments</key>
<array>
<string>/usr/bin/ssh</string>
<string>-N</string>
<string>-o</string>
<string>ExitOnForwardFailure=yes</string>
<string>-o</string>
<string>ServerAliveInterval=30</string>
<string>-o</string>
<string>ServerAliveCountMax=3</string>
<string>-o</string>
<string>StrictHostKeyChecking=accept-new</string>
<string>-L</string>
<string>127.0.0.1:55435:127.0.0.1:55435</string>
<string>root@179.197.228.240</string>
</array>
<key>RunAtLoad</key>
<true/>
<key>KeepAlive</key>
<dict>
<key>SuccessfulExit</key>
<false/>
</dict>
<key>ThrottleInterval</key>
<integer>15</integer>
<key>StandardOutPath</key>
<string>/tmp/doza-rag-tunnel.log</string>
<key>StandardErrorPath</key>
<string>/tmp/doza-rag-tunnel.log</string>
</dict>
</plist>
+28
View File
@@ -0,0 +1,28 @@
#!/bin/bash
# Garante que o tunel SSH até o Postgres da VPS (rag-hub-db) está aberto em
# 127.0.0.1:55435. Idempotente: se já estiver escutando, não faz nada.
# Usado pelo deploy e pode ser rodado à mão a qualquer momento.
HOST="root@179.197.228.240"
LOCAL_PORT=55435
REMOTE_PORT=55435
if nc -z 127.0.0.1 "$LOCAL_PORT" 2>/dev/null; then
echo "[rag] tunel ja aberto em 127.0.0.1:$LOCAL_PORT"
exit 0
fi
echo "[rag] abrindo tunel SSH ate $HOST ($LOCAL_PORT -> $REMOTE_PORT)..."
ssh -f -N -o ExitOnForwardFailure=yes -o ServerAliveInterval=30 -o ServerAliveCountMax=3 \
-L "127.0.0.1:${LOCAL_PORT}:127.0.0.1:${REMOTE_PORT}" "$HOST"
for _ in $(seq 1 10); do
if nc -z 127.0.0.1 "$LOCAL_PORT" 2>/dev/null; then
echo "[rag] tunel ativo."
exit 0
fi
sleep 0.5
done
echo "[rag] AVISO: nao foi possivel confirmar o tunel." >&2
exit 1
+444
View File
@@ -0,0 +1,444 @@
"""Indexador RAG do projeto.
Varre um diretório de código, quebra cada arquivo em trechos, gera embeddings
via Ollama (`nomic-embed-text`, 768 dims — bate com `code_chunks.embedding
vector(768)`) e grava tudo no Postgres da VPS (via túnel SSH).
Uso:
rag/index_tigre.sh # incremental (só o que mudou)
rag/index_tigre.sh --full # reindexa tudo, ignorando o hash
rag/index_tigre.sh --file codeclass/Sources/TigreAI/OllamaClient.swift
Reindexação é incremental: um hash (md5) do conteúdo de cada arquivo fica
salvo em `code_chunks.content_hash`. Se o hash não mudou, o arquivo é pulado.
Arquivos apagados do repo também têm seus chunks removidos.
Como o corte é feito
--------------------
Arquivos `.swift` são cortados por declaração (`swift_chunker`), não por
janela cega de linhas: cada trecho começa e termina em fronteira de código e
guarda `start_line`/`end_line`. É isso que permite ao agente ler só a janela
relevante depois da busca, em vez do arquivo inteiro. Os demais formatos
usam janela de linhas, também com faixa registrada.
Prefixos do embedding
---------------------
O nomic-embed-text espera `search_document: ` no que é indexado e
`search_query: ` no que é consultado. Sem isso a qualidade cai. Trocar de
regime invalida os vetores antigos, então quem muda isso precisa reindexar
com `--full`.
"""
import argparse
import hashlib
import os
import sys
import psycopg2
import requests
from dotenv import load_dotenv
RAG_DIR = os.path.dirname(os.path.abspath(__file__))
PROJECT_ROOT = os.path.dirname(RAG_DIR)
DEFAULT_TARGET = os.path.join(PROJECT_ROOT, "code")
if RAG_DIR not in sys.path:
sys.path.insert(0, RAG_DIR)
# Arquivo de env a carregar. Por padrão usa o .env do rag (sistema "doza",
# alvo code/). Um wrapper por sistema pode apontar para outro arquivo env
# (ex: RAG_ENV_FILE=tigre.env) e setar RAG_TARGET_DIR para o seu diretório.
_env_file = os.environ.get("RAG_ENV_FILE", os.path.join(RAG_DIR, ".env"))
load_dotenv(_env_file)
from swift_chunker import chunk_swift # noqa: E402
from swift_chunker import file_facts as swift_facts # noqa: E402
from python_chunker import chunk_python # noqa: E402
from python_chunker import file_facts as python_facts # noqa: E402
from ts_chunker import chunk_ts # noqa: E402
from ts_chunker import file_facts as ts_facts # noqa: E402
OLLAMA_URL = os.environ.get("OLLAMA_URL", "http://localhost:11434")
EMBED_MODEL = os.environ.get("RAG_EMBED_MODEL", "nomic-embed-text")
EMBED_DIM = 768
# Prefixos exigidos pelo nomic-embed-text. Exportados para o search.py usar
# o par correspondente na consulta.
DOC_PREFIX = "search_document: "
QUERY_PREFIX = "search_query: "
# Os prefixos só podem ser usados se o índice TAMBÉM foi construído com eles
# — misturar os dois regimes deixa consulta e documento em espaços
# diferentes. Ligado por padrão (é o comportamento correto para um índice
# novo); o `.env` do banco legado `doza`, indexado antes disso, desliga com
# RAG_EMBED_PREFIX=0.
USE_PREFIX = os.environ.get("RAG_EMBED_PREFIX", "1").lower() not in ("0", "false", "no")
# Extensões consideradas "código/documentação" para fins de RAG. Permite
# sobrescrever por env (ex: RAG_INCLUDE_EXT=".swift,.py,.md").
_include_ext_csv = os.environ.get("RAG_INCLUDE_EXT", ".py,.md,.sql,.sh,.command,.txt,.swift")
_INCLUDE_EXT = {e.strip() for e in _include_ext_csv.split(",") if e.strip()}
# Diretórios que nunca devem ser indexados (ambientes virtuais, caches,
# artefatos de build, dados de usuário).
_exclude_dir_csv = os.environ.get(
"RAG_EXCLUDE_DIRS",
".venv,.venv.bak,__pycache__,.pytest_cache,.git,node_modules,projects,"
"exports,dist,build,icon_build,.github,.build,DerivedData,.swiftpm"
)
_EXCLUDE_DIRS = {e.strip() for e in _exclude_dir_csv.split(",") if e.strip()}
# Lockfiles y artefactos de dependencias por nombre: ruido puro para RAG
# (JSON de resolución de dependencias, no el código que importa).
_EXCLUDE_FILES = {
"package-lock.json", "pnpm-lock.yaml", "yarn.lock", "npm-shrinkwrap.json",
"poetry.lock", "Pipfile.lock", "composer.lock", "composer.lock.json",
"Cargo.lock", "Gemfile.lock", "go.sum", "uv.lock",
}
_CHUNK_LINES = 60
_CHUNK_OVERLAP = 10
# nomic-embed-text's context window is 2048 tokens (~4 chars/token). Keep a
# safety margin below that so long lines (minified JS, long docstrings)
# don't blow the limit even when the line count is small.
_CHUNK_MAX_CHARS = 5000
def _qid(schema):
"""Schema como identificador SQL seguro (aspas duplas) — aceita guífen
(ex: `jhonny-rag`) sem quebrar a sintaxe. Retroativo: produtos sem guífen
voltam idênticos (`doza` -> `"doza"`)."""
return '"' + schema.replace('"', '""') + '"'
def _db_connect():
return psycopg2.connect(
host=os.environ["RAG_DB_HOST"],
port=os.environ["RAG_DB_PORT"],
dbname=os.environ["RAG_DB_NAME"],
user=os.environ["RAG_DB_USER"],
password=os.environ["RAG_DB_PASSWORD"],
connect_timeout=5,
)
def _iter_files(target_dir):
for root, dirs, files in os.walk(target_dir):
dirs[:] = [d for d in dirs if d not in _EXCLUDE_DIRS and not d.startswith(".")]
for name in files:
if name in _EXCLUDE_FILES:
continue
if os.path.splitext(name)[1] in _INCLUDE_EXT:
yield os.path.join(root, name)
def _module_of(rel_path):
"""Módulo SwiftPM do arquivo: o diretório logo abaixo de `Sources/`."""
parts = rel_path.split(os.sep)
if "Sources" in parts:
i = parts.index("Sources")
if i + 1 < len(parts) - 1:
return parts[i + 1]
return parts[1] if len(parts) > 2 else None
def _chunk_windows(text, chunk_lines=_CHUNK_LINES, overlap=_CHUNK_OVERLAP,
max_chars=_CHUNK_MAX_CHARS):
"""Corte por janela de linhas — usado fora do Swift.
Devolve dicts no mesmo formato do `chunk_swift`, com a faixa de linhas
preenchida, para todo o resto do pipeline não precisar saber qual dos
dois cortadores produziu o trecho.
"""
lines = text.splitlines()
if not lines:
return []
chunks = []
step = max(1, chunk_lines - overlap)
for start in range(0, len(lines), step):
window = lines[start:start + chunk_lines]
# Teto de caracteres aplicado POR LINHA, nunca no meio de uma linha,
# para start_line/end_line continuarem verdadeiros.
buf, buf_start, size = [], start, 0
for offset, line in enumerate(window):
if buf and size + len(line) + 1 > max_chars:
body = "\n".join(buf).strip()
if body:
chunks.append({
"content": body,
"start_line": buf_start + 1,
"end_line": buf_start + len(buf),
"symbols": None,
"kind": "window",
})
buf, buf_start, size = [], start + offset, 0
buf.append(line)
size += len(line) + 1
body = "\n".join(buf).strip()
if body:
chunks.append({
"content": body,
"start_line": buf_start + 1,
"end_line": buf_start + len(buf),
"symbols": None,
"kind": "window",
})
if start + chunk_lines >= len(lines):
break
return chunks
def chunk_file(text, rel_path):
"""Escolhe o cortador certo para o arquivo."""
ext = os.path.splitext(rel_path)[1]
cutters = {
".swift": chunk_swift, ".py": chunk_python,
".ts": chunk_ts, ".tsx": chunk_ts, ".js": chunk_ts, ".jsx": chunk_ts,
}
cutter = cutters.get(ext)
if cutter:
chunks = cutter(text, rel_path)
if chunks:
return chunks
return _chunk_windows(text)
def _generic_facts(text, rel_path):
"""Fatos de arquivos sem cortador próprio (.md, .sh, .sql, .txt).
O resumo é a primeira linha com conteúdo — num Markdown isso é o título,
num shell script o comentário de cabeçalho.
"""
lines = text.splitlines()
summary = None
for line in lines[:30]:
stripped = line.strip().lstrip("#!").lstrip("# ").strip()
if stripped and not stripped.startswith(("/bin/", "/usr/")):
summary = stripped
break
return {
"main_type": os.path.splitext(os.path.basename(rel_path))[0],
"summary": summary,
"public_symbols": [],
"n_lines": len(lines),
}
def file_facts(text, rel_path):
"""Fatos do arquivo para o mapa (`file_index`), por linguagem."""
ext = os.path.splitext(rel_path)[1]
if ext == ".swift":
return swift_facts(text, rel_path)
if ext == ".py":
return python_facts(text, rel_path)
if ext in (".ts", ".tsx", ".js", ".jsx"):
return ts_facts(text, rel_path)
return _generic_facts(text, rel_path)
def _embed(text, prefix=QUERY_PREFIX):
"""Embedding via Ollama.
O prefixo padrão é o de CONSULTA porque quem importa esta função de fora
(search.py) está sempre consultando; a indexação passa `DOC_PREFIX`
explicitamente.
"""
prompt = f"{prefix}{text}" if USE_PREFIX else text
resp = requests.post(
f"{OLLAMA_URL}/api/embeddings",
json={"model": EMBED_MODEL, "prompt": prompt},
timeout=60,
)
resp.raise_for_status()
vec = resp.json()["embedding"]
if len(vec) != EMBED_DIM:
raise ValueError(f"embedding com {len(vec)} dims, esperado {EMBED_DIM}")
return vec
def _file_hash(text):
return hashlib.md5(text.encode("utf-8")).hexdigest()
def _has_file_index(cur, schema):
"""O mapa de arquivos só existe onde a migration 002 rodou (schema tigre).
O banco legado (doza) segue sem ele; o indexador é o mesmo para os dois.
"""
cur.execute("SELECT to_regclass(%s)", (f"{_qid(schema)}.file_index",))
return cur.fetchone()[0] is not None
def index_project(target_dir=DEFAULT_TARGET, full=False, only_file=None):
conn = _db_connect()
conn.autocommit = False
schema = os.environ.get("RAG_DB_SCHEMA", "doza")
files_done = files_skipped = chunks_done = 0
stale_paths = set()
try:
with conn.cursor() as cur:
has_map = _has_file_index(cur, schema)
if not has_map:
print(" [aviso] sem tabela file_index neste schema — modo mapa "
"indisponivel (rode rag/migrate_tigre.sh)")
seen_paths = set()
paths = ([os.path.join(PROJECT_ROOT, only_file)] if only_file
else sorted(_iter_files(target_dir)))
for path in paths:
rel_path = os.path.relpath(path, PROJECT_ROOT)
try:
with open(path, encoding="utf-8", errors="ignore") as f:
text = f.read()
except OSError as exc:
print(f" skip {rel_path}: {exc}", file=sys.stderr)
continue
seen_paths.add(rel_path)
content_hash = _file_hash(text)
mtime = os.path.getmtime(path)
if not full:
cur.execute(
f"SELECT DISTINCT content_hash FROM {_qid(schema)}.code_chunks "
f"WHERE file_path = %s LIMIT 1",
(rel_path,),
)
row = cur.fetchone()
if row and row[0] == content_hash:
files_skipped += 1
print(f" [SKIP] {rel_path} sem alteracao")
continue
module = _module_of(rel_path)
chunks = chunk_file(text, rel_path)
facts = file_facts(text, rel_path)
# Prefixo de contexto no texto QUE VAI PARA O EMBEDDING (o
# `content` gravado continua sendo o código puro). Sem isso o
# doc-comment do tipo fica diluído no meio do código e
# consultas conceituais erram: "detectar rostos" não achava o
# FacePerceiver, cujo doc diz exatamente "Percepção de rostos".
context = " · ".join(
p for p in (rel_path, facts["main_type"], facts["summary"]) if p
)
cur.execute(
f"DELETE FROM {_qid(schema)}.code_chunks WHERE file_path = %s",
(rel_path,),
)
inserted = 0
for i, chunk in enumerate(chunks):
try:
embedding = _embed(f"{context}\n{chunk['content']}",
prefix=DOC_PREFIX)
except (requests.RequestException, ValueError) as exc:
print(f" skip chunk {rel_path}#{i}: {exc}", file=sys.stderr)
continue
cur.execute(
f"""
INSERT INTO {_qid(schema)}.code_chunks
(file_path, content, chunk_index, embedding,
content_hash, file_mtime, start_line, end_line,
symbols, module, kind)
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
""",
(rel_path, chunk["content"], i, embedding, content_hash,
mtime, chunk["start_line"], chunk["end_line"],
chunk["symbols"], module, chunk["kind"]),
)
inserted += 1
chunks_done += 1
if has_map:
# Embedding só do resumo — sem corpo de código para
# diluir. É a terceira lista da busca, a que resgata
# arquivos pequenos e precisos que os arquivos grandes
# abafam na lista densa por chunk.
try:
summary_emb = _embed(context, prefix=DOC_PREFIX)
except (requests.RequestException, ValueError) as exc:
print(f" skip resumo {rel_path}: {exc}", file=sys.stderr)
summary_emb = None
cur.execute(
f"""
INSERT INTO {_qid(schema)}.file_index
(file_path, module, main_type, public_symbols,
summary, n_lines, content_hash,
summary_embedding, updated_at)
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, now())
ON CONFLICT (file_path) DO UPDATE SET
module = EXCLUDED.module,
main_type = EXCLUDED.main_type,
public_symbols = EXCLUDED.public_symbols,
summary = EXCLUDED.summary,
n_lines = EXCLUDED.n_lines,
content_hash = EXCLUDED.content_hash,
summary_embedding = EXCLUDED.summary_embedding,
updated_at = now()
""",
(rel_path, module, facts["main_type"],
facts["public_symbols"], facts["summary"],
facts["n_lines"], content_hash, summary_emb),
)
files_done += 1
print(f" {rel_path}: {inserted} trecho(s)")
# Arquivos que sumiram do repo desde a última rodada: limpa os
# chunks, senão o índice acumula lixo morto. Não se aplica ao
# modo --file, que só enxerga um arquivo.
if not only_file:
cur.execute(f"SELECT DISTINCT file_path FROM {_qid(schema)}.code_chunks")
stale_paths = {r[0] for r in cur.fetchall()} - seen_paths
for rel_path in stale_paths:
cur.execute(
f"DELETE FROM {_qid(schema)}.code_chunks WHERE file_path = %s",
(rel_path,),
)
if has_map:
cur.execute(
f"DELETE FROM {_qid(schema)}.file_index WHERE file_path = %s",
(rel_path,),
)
print(f" [DELETE] removido (nao existe mais): {rel_path}")
conn.commit()
except Exception:
conn.rollback()
raise
finally:
conn.close()
print(
f"\nOK: {files_done} arquivo(s) reindexado(s), {files_skipped} sem mudanca "
f"(pulado(s)), {len(stale_paths)} removido(s), {chunks_done} trecho(s) gravado(s)."
)
def main():
ap = argparse.ArgumentParser(description="Indexador RAG")
ap.add_argument("target", nargs="?", default=None,
help="diretorio a indexar (padrao: RAG_TARGET_DIR ou code/)")
ap.add_argument("--full", action="store_true",
help="reindexa tudo, ignorando o hash de conteudo")
ap.add_argument("--file", default=None,
help="reindexa um unico arquivo (caminho relativo a raiz)")
args = ap.parse_args()
# Alvo prioritário: RAG_TARGET_DIR da env (ex: codeclass/Sources). Pode
# ser relativo ao project root; senão, argumento CLI; por último code/.
env_target = os.environ.get("RAG_TARGET_DIR")
default_target = (os.path.join(PROJECT_ROOT, env_target) if env_target
else DEFAULT_TARGET)
target = args.target or default_target
db = os.environ.get("RAG_DB_NAME", "?")
schema = os.environ.get("RAG_DB_SCHEMA", "?")
what = args.file or target
mode = " (--full)" if args.full else ""
print(f"Indexando {what} -> {db}.{schema}{mode}...")
index_project(target, full=args.full, only_file=args.file)
if __name__ == "__main__":
main()
+10
View File
@@ -0,0 +1,10 @@
#!/bin/bash
# Indexa o codigo do sistema legado (code/) no banco RAG rag_doza.
# Uso: rag/index_doza.sh [--full] [--file <caminho>]
set -e
RAG_DIR="$(cd "$(dirname "$0")" && pwd)"
"$RAG_DIR/ensure_tunnel.sh" >&2
PY="${RAG_DIR}/.venv/bin/python3"
[ -x "$PY" ] || PY="$(command -v python3)"
export RAG_ENV_FILE="$RAG_DIR/.env"
exec "$PY" "$RAG_DIR/index_code.py" "$@"
+10
View File
@@ -0,0 +1,10 @@
#!/bin/bash
# Indexa o codigo do projeto Jhonny (code/) no banco RAG jhonny-rag.
# Uso: rag/index_jhonny.sh [--full] [--file <caminho>]
set -e
RAG_DIR="$(cd "$(dirname "$0")" && pwd)"
"$RAG_DIR/ensure_tunnel.sh" >&2
PY="${RAG_DIR}/.venv/bin/python3"
[ -x "$PY" ] || PY="$(command -v python3)"
export RAG_ENV_FILE="$RAG_DIR/jhonny.env"
exec "$PY" "$RAG_DIR/index_code.py" "$@"
+20
View File
@@ -0,0 +1,20 @@
#!/bin/bash
# Indexa o código do novo sistema Tigre (codeclass/Sources) no banco RAG
# dedicado (rag_tigre, schema tigre). Usa o mesmo container compartilhado
# rag-hub-db — um banco por sistema.
set -e
RAG_DIR="$(cd "$(dirname "$0")" && pwd)"
PROJECT_ROOT="$(dirname "$RAG_DIR")"
# Garante o túnel SSH até o Postgres da VPS (idempotente).
"$RAG_DIR/ensure_tunnel.sh" >&2
# usa o python do ag .venv se existir, senão o python3 global
PY="${RAG_DIR}/.venv/bin/python3"
if [ ! -x "$PY" ]; then
PY="$(command -v python3)"
fi
export RAG_ENV_FILE="$RAG_DIR/tigre.env"
exec "$PY" "$RAG_DIR/index_code.py" "$@"
+12
View File
@@ -0,0 +1,12 @@
RAG_DB_HOST=127.0.0.1
RAG_DB_PORT=55435
RAG_DB_NAME=jhonny-rag
RAG_DB_USER=jhonny-rag
RAG_DB_PASSWORD=H46SlP8AbMo-LDefYZwgPnSLHcAWtllT
RAG_DB_SCHEMA=jhonny-rag
RAG_EMBED_PREFIX=1
# Projeto Jhonny é TypeScript (MCP, code/): incluir .ts/.js/.json além do
# default (.py,.md,.sql,.sh,.command,.txt,.swift). node_modules e dist já
# são excluídos por default (_EXCLUDE_DIRS no index_code.py).
RAG_INCLUDE_EXT=.py,.md,.sql,.sh,.command,.txt,.swift,.ts,.tsx,.js,.jsx,.json
Executable
+42
View File
@@ -0,0 +1,42 @@
#!/bin/bash
# Aplica uma migration SQL num banco do rag-hub como rag_admin.
#
# Não chame direto — use os wrappers por sistema (`migrate_tigre.sh`,
# `migrate_doza.sh`), que já sabem o nome do banco.
#
# A senha do rag_admin vive só em /docker/rag-hub/.env NA VPS. Este script
# lê a senha lá dentro e a entrega ao psql pelo ambiente do processo remoto:
# ela nunca aparece na linha de comando, em arquivo local, nem no histórico
# do shell — como exige admin/VPS-ACCESS.md § Segurança.
#
# Uso: migrate.sh <banco> <schema> <arquivo.sql>
set -euo pipefail
VPS="root@179.197.228.240"
DB="$1"
SCHEMA="$2"
SQL_FILE="$3"
if [ ! -f "$SQL_FILE" ]; then
echo "[rag] migration nao encontrada: $SQL_FILE" >&2
exit 1
fi
echo "[rag] aplicando $(basename "$SQL_FILE") em $DB (como rag_admin)..."
# O SQL vai por stdin do ssh; a senha é resolvida no lado remoto.
ssh "$VPS" "
set -e
cd /docker/rag-hub
PGPASSWORD=\$(grep -E '^POSTGRES_PASSWORD=' .env | cut -d= -f2-) \
docker exec -i -e PGPASSWORD rag-hub-db \
psql -v ON_ERROR_STOP=1 -U rag_admin -d $DB -f -
" < "$SQL_FILE"
echo "[rag] migration aplicada. Indices agora no schema $SCHEMA:"
ssh "$VPS" "
cd /docker/rag-hub
PGPASSWORD=\$(grep -E '^POSTGRES_PASSWORD=' .env | cut -d= -f2-) \
docker exec -i -e PGPASSWORD rag-hub-db \
psql -U rag_admin -d $DB -c \"SELECT indexname FROM pg_indexes WHERE schemaname='$SCHEMA' ORDER BY 1;\"
"
+10
View File
@@ -0,0 +1,10 @@
#!/bin/bash
# Aplica uma migration no banco do sistema legado (rag_doza, schema doza).
# Uso: rag/migrate_doza.sh [arquivo.sql] (padrao: a 002-doza)
set -euo pipefail
RAG_DIR="$(cd "$(dirname "$0")" && pwd)"
"$RAG_DIR/migrate.sh" rag_doza doza \
"${1:-$RAG_DIR/migrations/002-doza-search.sql}"
echo
echo "[rag] PROXIMO PASSO OBRIGATORIO: reindexar do zero."
echo " rag/index_doza.sh --full"
+10
View File
@@ -0,0 +1,10 @@
#!/bin/bash
# Aplica uma migration no banco do projeto Jhonny (jhonny-rag, schema jhonny-rag).
# Uso: rag/migrate_jhonny.sh [arquivo.sql] (padrao: schema-jhonny.sql)
set -euo pipefail
RAG_DIR="$(cd "$(dirname "$0")" && pwd)"
"$RAG_DIR/migrate.sh" "jhonny-rag" "jhonny-rag" \
"${1:-$RAG_DIR/schema-jhonny.sql}"
echo
echo "[rag] PROXIMO PASSO OBRIGATORIO: reindexar do zero."
echo " rag/index_jhonny.sh --full"
+10
View File
@@ -0,0 +1,10 @@
#!/bin/bash
# Aplica uma migration no banco do sistema novo (rag_tigre, schema tigre).
# Uso: rag/migrate_tigre.sh [arquivo.sql] (padrao: a 002)
set -euo pipefail
RAG_DIR="$(cd "$(dirname "$0")" && pwd)"
"$RAG_DIR/migrate.sh" rag_tigre tigre \
"${1:-$RAG_DIR/migrations/002-tigre-search.sql}"
echo
echo "[rag] PROXIMO PASSO OBRIGATORIO: reindexar do zero."
echo " rag/index_tigre.sh --full"
+75
View File
@@ -0,0 +1,75 @@
-- Migration 002 (doza) — busca híbrida e leitura cirúrgica no schema `doza`.
--
-- É a mesma mudança já aplicada ao schema `tigre` (migrations 002 e 003),
-- reunida num arquivo só porque aqui as duas vão juntas desde o início.
--
-- Motivo, medido no índice legado (694 trechos / 93 arquivos):
-- O índice ivfflat com lists=100 sobre 694 linhas dá ~7 linhas por lista.
-- Com o padrão ivfflat.probes=1 a busca varre quase nada, e o resultado é
-- ruído: procurar `SpineSegment` devolvia requirements.txt e CLA.md,
-- enquanto a busca exata põe doza_assist/fcpxml/parser.py em 1º.
-- recall@5 do conjunto dourado (rag/bench_doza.sh): 0%. Zero de 18.
--
-- Idempotente: seguro rodar mais de uma vez.
--
-- COMO RODAR (precisa ser rag_admin — o rag_doza_indexer não tem DDL):
-- rag/migrate_doza.sh
--
-- DEPOIS DE RODAR: reindexar do zero é obrigatório.
-- rag/index_doza.sh --full
-- Os embeddings passam a usar os prefixos do nomic-embed-text; vetores
-- antigos e novos não são comparáveis entre si.
BEGIN;
-- 1. Índice vetorial: ivfflat -> HNSW ---------------------------------
DROP INDEX IF EXISTS doza.code_chunks_embedding_idx;
CREATE INDEX IF NOT EXISTS code_chunks_embedding_hnsw_idx
ON doza.code_chunks USING hnsw (embedding vector_cosine_ops)
WITH (m = 16, ef_construction = 64);
-- 2. Metadados de trecho ----------------------------------------------
ALTER TABLE doza.code_chunks ADD COLUMN IF NOT EXISTS start_line int;
ALTER TABLE doza.code_chunks ADD COLUMN IF NOT EXISTS end_line int;
ALTER TABLE doza.code_chunks ADD COLUMN IF NOT EXISTS symbols text;
ALTER TABLE doza.code_chunks ADD COLUMN IF NOT EXISTS module text;
ALTER TABLE doza.code_chunks ADD COLUMN IF NOT EXISTS kind text;
ALTER TABLE doza.code_chunks ADD COLUMN IF NOT EXISTS file_mtime double precision;
-- 3. Índices lexicos (pg_trgm) — o lado keyword da busca híbrida -------
CREATE INDEX IF NOT EXISTS code_chunks_file_path_trgm_idx
ON doza.code_chunks USING gin (file_path gin_trgm_ops);
CREATE INDEX IF NOT EXISTS code_chunks_symbols_trgm_idx
ON doza.code_chunks USING gin (symbols gin_trgm_ops);
CREATE INDEX IF NOT EXISTS code_chunks_module_idx
ON doza.code_chunks (module);
CREATE INDEX IF NOT EXISTS idx_code_chunks_file_path
ON doza.code_chunks (file_path);
-- 4. Mapa de arquivos, com embedding do resumo ------------------------
CREATE TABLE IF NOT EXISTS doza.file_index (
file_path text PRIMARY KEY,
module text,
main_type text,
public_symbols text[],
summary text,
n_lines int,
content_hash text,
summary_embedding vector(768),
updated_at timestamp DEFAULT now()
);
CREATE INDEX IF NOT EXISTS file_index_module_idx
ON doza.file_index (module);
CREATE INDEX IF NOT EXISTS file_index_path_trgm_idx
ON doza.file_index USING gin (file_path gin_trgm_ops);
CREATE INDEX IF NOT EXISTS file_index_summary_hnsw_idx
ON doza.file_index USING hnsw (summary_embedding vector_cosine_ops)
WITH (m = 16, ef_construction = 64);
-- 5. Permissões do usuário de indexação/busca -------------------------
GRANT SELECT, INSERT, UPDATE, DELETE ON doza.file_index TO rag_doza_indexer;
GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA doza TO rag_doza_indexer;
COMMIT;
+96
View File
@@ -0,0 +1,96 @@
-- Migration 002 — busca hibrida e leitura cirurgica no schema `tigre`.
--
-- Motivo (medido em 02/09/2026, 346 chunks / 190 arquivos):
-- 1. O indice ivfflat com lists=100 sobre 346 linhas da ~3,5 linhas por
-- lista. Com o padrao ivfflat.probes=1 a busca varre ~3 de 346 linhas,
-- e varias consultas voltavam VAZIAS. recall@5 do conjunto dourado
-- (rag/bench.py): 33%. Trocamos por HNSW, que nao particiona.
-- 2. A busca densa pura erra nomes exatos de tipo (`FCPXMLValidator` nao
-- aparecia no top-5 do proprio arquivo). Adicionamos `symbols` +
-- indices GIN trgm para o lado lexico da fusao.
-- 3. Os chunks nao guardavam faixa de linhas, entao o agente lia o
-- arquivo inteiro depois da busca. `start_line`/`end_line` permitem
-- ler so a janela relevante.
--
-- Idempotente: seguro rodar mais de uma vez.
--
-- COMO RODAR (precisa ser rag_admin — o rag_tigre_indexer nao tem DDL):
--
-- rag/ensure_tunnel.sh
-- psql -h 127.0.0.1 -p 55435 -U rag_admin -d rag_tigre \
-- -f rag/migrations/002-tigre-search.sql
--
-- A senha do rag_admin esta em admin/VPS-ACCESS.md.
--
-- DEPOIS DE RODAR: reindexar do zero e obrigatorio.
-- rag/reindex_tigre.sh --full
-- Os embeddings passam a usar o prefixo `search_document: ` exigido pelo
-- nomic-embed-text; vetores antigos e novos nao sao comparaveis entre si.
BEGIN;
-- ---------------------------------------------------------------------
-- 1. Indice vetorial: ivfflat -> HNSW
-- ---------------------------------------------------------------------
DROP INDEX IF EXISTS tigre.code_chunks_embedding_idx;
CREATE INDEX IF NOT EXISTS code_chunks_embedding_hnsw_idx
ON tigre.code_chunks USING hnsw (embedding vector_cosine_ops)
WITH (m = 16, ef_construction = 64);
-- ---------------------------------------------------------------------
-- 2. Metadados de chunk: faixa de linhas, simbolos, modulo, tipo
-- ---------------------------------------------------------------------
ALTER TABLE tigre.code_chunks ADD COLUMN IF NOT EXISTS start_line int;
ALTER TABLE tigre.code_chunks ADD COLUMN IF NOT EXISTS end_line int;
ALTER TABLE tigre.code_chunks ADD COLUMN IF NOT EXISTS symbols text;
ALTER TABLE tigre.code_chunks ADD COLUMN IF NOT EXISTS module text;
-- kind: 'decl' (declaracao Swift), 'header' (imports + doc do tipo),
-- 'window' (fallback por janela de linhas, usado fora do Swift)
ALTER TABLE tigre.code_chunks ADD COLUMN IF NOT EXISTS kind text;
-- ---------------------------------------------------------------------
-- 3. Indices lexicos (pg_trgm) — o lado keyword da busca hibrida
-- ---------------------------------------------------------------------
CREATE INDEX IF NOT EXISTS code_chunks_file_path_trgm_idx
ON tigre.code_chunks USING gin (file_path gin_trgm_ops);
CREATE INDEX IF NOT EXISTS code_chunks_symbols_trgm_idx
ON tigre.code_chunks USING gin (symbols gin_trgm_ops);
-- Filtro de escopo por modulo (--module TigreAI).
CREATE INDEX IF NOT EXISTS code_chunks_module_idx
ON tigre.code_chunks (module);
-- ---------------------------------------------------------------------
-- 4. Mapa de arquivos — 1 linha por arquivo, para responder "onde fica X"
-- sem trazer nenhum corpo de codigo.
-- ---------------------------------------------------------------------
CREATE TABLE IF NOT EXISTS tigre.file_index (
file_path text PRIMARY KEY,
module text,
main_type text,
public_symbols text[],
summary text,
n_lines int,
content_hash text,
updated_at timestamp DEFAULT now()
);
CREATE INDEX IF NOT EXISTS file_index_module_idx
ON tigre.file_index (module);
CREATE INDEX IF NOT EXISTS file_index_path_trgm_idx
ON tigre.file_index USING gin (file_path gin_trgm_ops);
-- ---------------------------------------------------------------------
-- 5. Permissoes do usuario de indexacao/busca
-- ---------------------------------------------------------------------
GRANT SELECT, INSERT, UPDATE, DELETE ON tigre.file_index TO rag_tigre_indexer;
GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA tigre TO rag_tigre_indexer;
COMMIT;
-- Conferencia rapida (rode a mao depois):
-- \d tigre.code_chunks
-- SELECT indexname FROM pg_indexes WHERE schemaname = 'tigre';
@@ -0,0 +1,28 @@
-- Migration 003 — embedding do resumo de cada arquivo.
--
-- Motivo (medido depois da 002): os scores do nomic-embed-text ficam
-- comprimidos numa faixa estreita (0,50–0,65 neste corpus), e arquivos
-- grandes viram "hubs" — casam morno com qualquer consulta e ocupam o topo.
-- Dois alvos legítimos caíam para as posições 33 e 63 da lista densa com
-- score praticamente colado no do 1º colocado:
--
-- "gravar arquivo em disco de forma atomica" -> AtomicFileIO (33º)
-- "impedir caminho de projeto fora da pasta ..." -> ProjectPathSafety (63º)
--
-- Um embedding só do resumo (caminho + tipo + doc-comment, sem corpo de
-- código para diluir) não sofre disso, e entra como terceira lista da fusão.
--
-- COMO RODAR:
-- rag/migrate_tigre.sh rag/migrations/003-file-summary-embedding.sql
-- DEPOIS: rag/index_tigre.sh --full
BEGIN;
ALTER TABLE tigre.file_index
ADD COLUMN IF NOT EXISTS summary_embedding vector(768);
CREATE INDEX IF NOT EXISTS file_index_summary_hnsw_idx
ON tigre.file_index USING hnsw (summary_embedding vector_cosine_ops)
WITH (m = 16, ef_construction = 64);
COMMIT;
+234
View File
@@ -0,0 +1,234 @@
"""Corte de arquivos Python em trechos por declaração, com faixa de linhas.
Irmão do `swift_chunker`, para o índice do sistema legado (`code/`). O
problema aqui é diferente: o legado NÃO segue "uma classe por arquivo" —
`ai_analysis.py` e `app.py` têm mais de 5000 linhas cada, com dezenas de
funções soltas. Cortar isso em janelas cegas de 60 linhas produz trechos que
começam no meio de uma função e não dizem a que função pertencem.
Python facilita: o nível de indentação já marca a estrutura, então um bloco
`def`/`class` vai da sua linha de cabeçalho até a próxima linha não vazia com
indentação menor ou igual.
"""
import os
import re
# Início de um bloco nomeado, em qualquer nível de indentação.
_BLOCK_RE = re.compile(
r"^(?P<indent>[ \t]*)(?:async\s+)?(?P<kw>def|class)\s+(?P<name>[A-Za-z_]\w*)"
)
# Decoradores e comentários que pertencem ao bloco logo abaixo.
_PREAMBLE_RE = re.compile(r"^[ \t]*(?:@|#)")
# Abre uma docstring: prefixo opcional de string (r/b/u/f) seguido das
# aspas. O prefixo é capturado como grupo para ser removido com precisão —
# um `strip()` de conjunto de caracteres comeria o "F" de "FCPXML parser".
_DOCSTRING_RE = re.compile(r'^(?:[rRbBuUfF]{0,2})("""|\'\'\'|"|\')(?P<rest>.*)$')
WHOLE_FILE_MAX_LINES = 120
TARGET_CHARS = 2000
MAX_CHARS = 5000
def _blocks_at(lines, start, end, indent):
"""Fronteiras dos blocos `def`/`class` no nível de indentação dado.
Devolve lista de (início, fim) meio-aberta, já incluindo decoradores e
comentários que precedem cada bloco.
"""
starts = []
for i in range(start, end):
m = _BLOCK_RE.match(lines[i])
if not m or len(m.group("indent").expandtabs(4)) != indent:
continue
j = i
while j > start and _PREAMBLE_RE.match(lines[j - 1]):
j -= 1
if not starts or j > starts[-1]:
starts.append(j)
out = []
for idx, s in enumerate(starts):
e = starts[idx + 1] if idx + 1 < len(starts) else end
out.append((s, e))
return out
def _body_indent(lines, start, end):
"""Indentação do corpo do bloco que começa em `start`."""
for i in range(start, end):
stripped = lines[i].strip()
if not stripped or _PREAMBLE_RE.match(lines[i]):
continue
if _BLOCK_RE.match(lines[i]):
continue
return len(lines[i]) - len(lines[i].lstrip())
return 4
def _split_long(chunk_lines, start_line, max_chars=MAX_CHARS):
"""Divide um trecho grande em pedaços, sempre em fronteira de linha."""
out, buf, buf_start, size = [], [], start_line, 0
for offset, line in enumerate(chunk_lines):
if buf and size + len(line) + 1 > max_chars:
out.append((buf, buf_start))
buf, buf_start, size = [], start_line + offset, 0
buf.append(line)
size += len(line) + 1
if buf:
out.append((buf, buf_start))
return out
def _symbols_of(text, file_stem, module_parts):
"""Nomes buscáveis do trecho.
O nome do arquivo e as pastas do módulo entram sempre: no legado é comum
procurar por `exporters/edl` ou `media_probe`, que são caminho, não
identificador declarado.
"""
names = {file_stem} | set(module_parts)
names.update(m.group("name") for m in _BLOCK_RE.finditer(text))
return " ".join(sorted(n for n in names if n))
def _docstring_first_line(lines, start, end):
for i in range(start, min(end, start + 40)):
s = lines[i].strip()
if not s or s.startswith(("#", "@")):
continue
m = _DOCSTRING_RE.match(s)
if m:
quote = m.group(1)
text = m.group("rest")
if text.endswith(quote):
text = text[:-len(quote)]
text = text.strip()
if text:
return text
# Docstring cuja primeira linha é só as aspas: pega a seguinte.
if i + 1 < end:
nxt = lines[i + 1].strip()
if nxt.endswith(quote):
nxt = nxt[:-len(quote)]
return nxt.strip() or None
return None
return None
return None
def file_facts(text, rel_path):
"""Tipo principal, símbolos públicos e resumo — alimenta `file_index`."""
lines = text.splitlines()
stem = os.path.splitext(os.path.basename(rel_path))[0]
main_type = None
public_symbols = []
for s, e in _blocks_at(lines, 0, len(lines), 0):
m = _BLOCK_RE.match(lines[s]) or next(
(_BLOCK_RE.match(lines[k]) for k in range(s, e)
if _BLOCK_RE.match(lines[k])), None)
if not m:
continue
name = m.group("name")
if not name.startswith("_"):
public_symbols.append(name)
if main_type is None and m.group("kw") == "class":
main_type = name
# Resumo: docstring do módulo; se não houver, a do primeiro bloco.
summary = _docstring_first_line(lines, 0, len(lines))
if not summary:
blocks = _blocks_at(lines, 0, len(lines), 0)
if blocks:
s, e = blocks[0]
body = _body_indent(lines, s, e)
summary = _docstring_first_line(lines, s + 1, e)
del body
return {
"main_type": main_type or stem,
"summary": summary,
"public_symbols": sorted(set(public_symbols)),
"n_lines": len(lines),
}
def _merge_small(chunks):
"""Junta blocos vizinhos curtos até `TARGET_CHARS`.
Sem isso cada função de três linhas (`_timebase`, `_actual_fps`…) vira um
trecho próprio: muitos embeddings e pouco contexto em cada resultado.
"""
merged = []
for c in chunks:
prev = merged[-1] if merged else None
if (prev is not None
and prev["end_line"] + 1 == c["start_line"]
and prev["kind"] == c["kind"] == "decl"
and len(prev["content"]) + len(c["content"]) + 1 <= TARGET_CHARS):
prev["content"] += "\n" + c["content"]
prev["end_line"] = c["end_line"]
prev["symbols"] = " ".join(
sorted(set(prev["symbols"].split()) | set(c["symbols"].split()))
)
else:
merged.append(dict(c))
return merged
def chunk_python(text, rel_path):
"""Divide um arquivo Python em trechos com faixa de linhas e símbolos.
Linhas são 1-indexadas e inclusivas nas duas pontas, iguais ao que o
`Read(offset=…, limit=…)` do agente espera.
"""
lines = text.splitlines()
if not lines:
return []
stem = os.path.splitext(os.path.basename(rel_path))[0]
module_parts = [p for p in os.path.dirname(rel_path).split(os.sep)
if p not in ("", "code")]
def build(start, end, kind):
out = []
for piece, piece_start in _split_long(lines[start:end], start + 1):
body = "\n".join(piece).strip()
if not body:
continue
out.append({
"content": body,
"start_line": piece_start,
"end_line": piece_start + len(piece) - 1,
"symbols": _symbols_of(body, stem, module_parts),
"kind": kind,
})
return out
if len(lines) <= WHOLE_FILE_MAX_LINES:
return build(0, len(lines), "file")
top = _blocks_at(lines, 0, len(lines), 0)
if not top:
return build(0, len(lines), "window")
# Cabeçalho: docstring do módulo + imports, antes do primeiro bloco.
chunks = build(0, top[0][0], "header") if top[0][0] > 0 else []
for s, e in top:
size = sum(len(lines[i]) + 1 for i in range(s, e))
if size <= TARGET_CHARS:
chunks.extend(build(s, e, "decl"))
continue
# Bloco grande (classe com muitos métodos): desce um nível e corta
# por método, mantendo a assinatura da classe no primeiro pedaço.
inner = _blocks_at(lines, s + 1, e, _body_indent(lines, s, e))
if not inner:
chunks.extend(build(s, e, "decl"))
continue
chunks.extend(build(s, inner[0][0], "decl"))
for si, ei in inner:
chunks.extend(build(si, ei, "decl"))
return _merge_small(chunks)
+40
View File
@@ -0,0 +1,40 @@
#!/bin/bash
# Hook PostToolUse: reindexa no RAG só o arquivo que acabou de ser editado.
#
# Sem isso o índice envelhece em silêncio e passa a apontar para código que
# já mudou — o que anula o ganho da busca. Reindexar um arquivo custa alguns
# embeddings (~1s), então dá para fazer a cada edição.
#
# Lê o JSON do hook em stdin e roteia por extensão/diretório para o banco
# certo (Swift novo -> rag_tigre, Python legado -> rag_doza). Nunca falha o
# hook: erro de RAG não pode travar edição.
RAG_DIR="$(cd "$(dirname "$0")" && pwd)"
PROJECT_ROOT="$(dirname "$RAG_DIR")"
FILE=$(python3 -c '
import json, sys
try:
d = json.load(sys.stdin)
except Exception:
sys.exit(0)
print(d.get("tool_input", {}).get("file_path", ""))
' 2>/dev/null) || exit 0
REL="${FILE#$PROJECT_ROOT/}"
case "$REL" in
code/node_modules/*|code/dist/*) exit 0 ;;
code/*.ts|code/*.tsx|code/*.js|code/*.jsx|code/*.json|code/*.py|code/*.md|code/*.sql|code/*.sh) ENV_FILE="jhonny.env" ;;
*) exit 0 ;;
esac
# Só reindexa se o túnel já estiver de pé: não vale abrir conexão SSH no
# meio de uma edição.
nc -z 127.0.0.1 55435 2>/dev/null || exit 0
PY="${RAG_DIR}/.venv/bin/python3"
[ -x "$PY" ] || PY="$(command -v python3)"
RAG_ENV_FILE="$RAG_DIR/$ENV_FILE" "$PY" "$RAG_DIR/index_code.py" \
--file "$REL" >/dev/null 2>&1 &
exit 0
+4
View File
@@ -0,0 +1,4 @@
# Dependências só do indexador RAG (ferramenta de dev, não da aplicação).
psycopg2-binary>=2.9
python-dotenv>=1.0
requests
+88
View File
@@ -0,0 +1,88 @@
-- Schema RAG do projeto Jhonny (projeto MCP TypeScript em code/).
-- Idempotente: seguro rodar múltiplas vezes (CREATE ... IF NOT EXISTS).
-- Segue o padrão do banco irmão rag_tigre (schema tigre): um banco por sistema
-- dentro do container compartilhado rag-hub-db, schema próprio.
--
-- Este arquivo é o estado FINAL desejado do schema — inclui o que as
-- migrations `002-*-search.sql` e `003-file-summary-embedding.sql` aplicaram
-- num banco já existente. Num banco novo, rodar só este arquivo basta.
--
-- O schema tem um hífen no nome ("jhonny-rag"), entao todos os identificadores
-- de schema sao escapados com aspas duplas.
CREATE EXTENSION IF NOT EXISTS vector;
CREATE EXTENSION IF NOT EXISTS pg_trgm;
CREATE SCHEMA IF NOT EXISTS "jhonny-rag";
CREATE TABLE IF NOT EXISTS "jhonny-rag".code_chunks (
id bigserial PRIMARY KEY,
file_path text NOT NULL,
content text NOT NULL,
chunk_index int,
embedding vector(768),
content_hash text,
file_mtime double precision,
-- Faixa de linhas do trecho no arquivo original. É o que permite ao
-- agente ler só a janela relevante em vez do arquivo inteiro.
start_line int,
end_line int,
-- Nomes declarados no trecho, separados por espaço — o lado lexical
-- (pg_trgm) da busca híbrida casa contra isto.
symbols text,
-- Módulo derivado do caminho relativo em code/ (ex: code/src/tools -> tools).
module text,
-- 'decl' | 'header' | 'window' (fallback por janela de linhas, usado fora
-- de Swift/Python — será o caso dos arquivos .ts/.js deste projeto).
kind text,
updated_at timestamp DEFAULT now()
);
-- HNSW, não ivfflat: com poucas centenas de chunks o ivfflat particiona o
-- espaço em listas quase vazias e a busca com probes=1 varre quase nada
-- (media das migrations 002 registra o recall baixo que isso causava).
CREATE INDEX IF NOT EXISTS code_chunks_embedding_hnsw_idx
ON "jhonny-rag".code_chunks USING hnsw (embedding vector_cosine_ops)
WITH (m = 16, ef_construction = 64);
-- Acelera a reindexação incremental (busca por file_path).
CREATE INDEX IF NOT EXISTS idx_code_chunks_file_path
ON "jhonny-rag".code_chunks (file_path);
-- Lado lexical da busca híbrida.
CREATE INDEX IF NOT EXISTS code_chunks_file_path_trgm_idx
ON "jhonny-rag".code_chunks USING gin (file_path gin_trgm_ops);
CREATE INDEX IF NOT EXISTS code_chunks_symbols_trgm_idx
ON "jhonny-rag".code_chunks USING gin (symbols gin_trgm_ops);
CREATE INDEX IF NOT EXISTS code_chunks_module_idx
ON "jhonny-rag".code_chunks (module);
-- Hash de conteúdo por arquivo, usado pelo indexador para pular arquivos
-- que não mudaram desde a última rodada (reindexação incremental).
CREATE TABLE IF NOT EXISTS "jhonny-rag".indexed_files (
file_path text PRIMARY KEY,
content_hash text NOT NULL,
updated_at timestamp DEFAULT now()
);
-- Mapa de arquivos: 1 linha por arquivo. Responde "onde fica X" e "o que
-- tem no módulo Y" sem trazer nenhum corpo de código.
CREATE TABLE IF NOT EXISTS "jhonny-rag".file_index (
file_path text PRIMARY KEY,
module text,
main_type text,
public_symbols text[],
summary text,
n_lines int,
content_hash text,
summary_embedding vector(768),
updated_at timestamp DEFAULT now()
);
CREATE INDEX IF NOT EXISTS file_index_module_idx
ON "jhonny-rag".file_index (module);
CREATE INDEX IF NOT EXISTS file_index_path_trgm_idx
ON "jhonny-rag".file_index USING gin (file_path gin_trgm_ops);
CREATE INDEX IF NOT EXISTS file_index_summary_hnsw_idx
ON "jhonny-rag".file_index USING hnsw (summary_embedding vector_cosine_ops)
WITH (m = 16, ef_construction = 64);
+81
View File
@@ -0,0 +1,81 @@
-- Schema RAG do projeto Tigre (novo sistema em classes, Swift em codeclass/).
-- Idempotente: seguro rodar múltiplas vezes (CREATE ... IF NOT EXISTS).
-- Segue o padrão do banco irmão rag_doza (schema doza): um banco por sistema
-- dentro do container compartilhado rag-hub-db, schema próprio.
--
-- Este arquivo é o estado FINAL desejado do schema — inclui o que a
-- migration `migrations/002-tigre-search.sql` aplicou num banco já
-- existente. Num banco novo, rodar só este arquivo basta.
CREATE EXTENSION IF NOT EXISTS vector;
CREATE EXTENSION IF NOT EXISTS pg_trgm;
CREATE SCHEMA IF NOT EXISTS tigre;
CREATE TABLE IF NOT EXISTS tigre.code_chunks (
id bigserial PRIMARY KEY,
file_path text NOT NULL,
content text NOT NULL,
chunk_index int,
embedding vector(768),
content_hash text,
file_mtime double precision,
-- Faixa de linhas do trecho no arquivo original. É o que permite ao
-- agente ler só a janela relevante em vez do arquivo inteiro.
start_line int,
end_line int,
-- Nomes declarados no trecho, separados por espaço — o lado lexical
-- (pg_trgm) da busca híbrida casa contra isto.
symbols text,
-- Módulo SwiftPM derivado de codeclass/Sources/<Módulo>/…
module text,
-- 'decl' (declaração Swift) | 'header' (imports + doc do tipo)
-- | 'window' (fallback por janela de linhas, usado fora do Swift)
kind text,
updated_at timestamp DEFAULT now()
);
-- HNSW, não ivfflat: com poucas centenas de chunks o ivfflat particiona o
-- espaço em listas quase vazias e a busca com probes=1 varre quase nada
-- (media 002 registra o recall@5 de 33% que isso causava).
CREATE INDEX IF NOT EXISTS code_chunks_embedding_hnsw_idx
ON tigre.code_chunks USING hnsw (embedding vector_cosine_ops)
WITH (m = 16, ef_construction = 64);
-- Acelera a reindexação incremental (busca por file_path).
CREATE INDEX IF NOT EXISTS idx_code_chunks_file_path
ON tigre.code_chunks (file_path);
-- Lado lexical da busca híbrida.
CREATE INDEX IF NOT EXISTS code_chunks_file_path_trgm_idx
ON tigre.code_chunks USING gin (file_path gin_trgm_ops);
CREATE INDEX IF NOT EXISTS code_chunks_symbols_trgm_idx
ON tigre.code_chunks USING gin (symbols gin_trgm_ops);
CREATE INDEX IF NOT EXISTS code_chunks_module_idx
ON tigre.code_chunks (module);
-- Hash de conteúdo por arquivo, usado pelo indexador para pular arquivos
-- que não mudaram desde a última rodada (reindexação incremental).
CREATE TABLE IF NOT EXISTS tigre.indexed_files (
file_path text PRIMARY KEY,
content_hash text NOT NULL,
updated_at timestamp DEFAULT now()
);
-- Mapa de arquivos: 1 linha por arquivo. Responde "onde fica X" e "o que
-- tem no módulo Y" sem trazer nenhum corpo de código.
CREATE TABLE IF NOT EXISTS tigre.file_index (
file_path text PRIMARY KEY,
module text,
main_type text,
public_symbols text[],
summary text,
n_lines int,
content_hash text,
updated_at timestamp DEFAULT now()
);
CREATE INDEX IF NOT EXISTS file_index_module_idx
ON tigre.file_index (module);
CREATE INDEX IF NOT EXISTS file_index_path_trgm_idx
ON tigre.file_index USING gin (file_path gin_trgm_ops);
+29
View File
@@ -0,0 +1,29 @@
-- Schema RAG do projeto Doza.
-- Idempotente: seguro rodar múltiplas vezes (CREATE ... IF NOT EXISTS).
-- Ao copiar este arquivo para um projeto novo, trocar "doza" pelo nome
-- do novo sistema no CREATE SCHEMA e nas referências de tabela abaixo.
CREATE EXTENSION IF NOT EXISTS vector;
CREATE EXTENSION IF NOT EXISTS pg_trgm;
CREATE SCHEMA IF NOT EXISTS doza;
CREATE TABLE IF NOT EXISTS doza.code_chunks (
id bigserial PRIMARY KEY,
file_path text NOT NULL,
content text NOT NULL,
chunk_index int,
embedding vector(768),
updated_at timestamp DEFAULT now()
);
CREATE INDEX IF NOT EXISTS code_chunks_embedding_idx
ON doza.code_chunks USING ivfflat (embedding vector_cosine_ops) WITH (lists = 100);
-- Hash de conteúdo por arquivo, usado pelo indexador para pular arquivos
-- que não mudaram desde a última rodada (reindexação incremental).
CREATE TABLE IF NOT EXISTS doza.indexed_files (
file_path text PRIMARY KEY,
content_hash text NOT NULL,
updated_at timestamp DEFAULT now()
);
+490
View File
@@ -0,0 +1,490 @@
"""Busca semântica RAG do projeto.
Consulta `<schema>.code_chunks` no Postgres (via túnel SSH) combinando dois
sinais e devolvendo faixas de linha, para o agente ler só o trecho relevante
em vez do arquivo inteiro.
Uso:
rag/search_tigre.sh "como funciona o export FCPXML"
rag/search_tigre.sh "FCPXMLValidator" --snippet
rag/search_tigre.sh "wizard" --module TigreAppUI --json
rag/search_tigre.sh --map TigreAI # inventário do módulo
Como módulo:
from search import rag_search
rag_search("consulta", top_k=5)
Por que busca híbrida
---------------------
A busca puramente densa erra nomes exatos: procurar `FCPXMLValidator` não
trazia `FCPXMLValidator.swift`, e `SilenceCutPipeline.swift` não aparecia em
nenhum top-5. Por isso rodamos duas listas em paralelo — densa (embedding) e
lexical (pg_trgm sobre os símbolos declarados) — e fundimos com RRF, que
soma 1/(k+posição) de cada lista e portanto não exige normalizar escalas
diferentes. Ver rag/bench.py para os números antes/depois.
"""
import argparse
import json
import os
import sys
import psycopg2
from dotenv import load_dotenv
RAG_DIR = os.path.dirname(os.path.abspath(__file__))
# Mesma mecânica do indexador: respeita RAG_ENV_FILE para permitir wrapper
# por sistema (ex: search_tigre.sh com RAG_ENV_FILE=tigre.env).
_load_path = os.environ.get("RAG_ENV_FILE", os.path.join(RAG_DIR, ".env"))
load_dotenv(_load_path)
if RAG_DIR not in sys.path:
sys.path.insert(0, RAG_DIR)
from index_code import QUERY_PREFIX, _embed # noqa: E402
# Constante do Reciprocal Rank Fusion e pesos por lista. Os valores saíram de
# uma varredura sobre o conjunto dourado (rag/bench.py), não da literatura: o
# k=60 clássico achata demais a curva para um corpus deste tamanho e deixava
# um 1º lugar isolado perder para um medíocre presente em duas listas.
# k=8 e peso 1.5 no resumo vêm de uma varredura (k × pesos) sobre as 18
# consultas de rag/bench.py. Não é o pico da grade — é o meio de um platô
# estável (k de 5 a 8 dá o mesmo recall, e em k=8 o resultado não muda com o
# peso do resumo entre 1.0 e 2.0), escolhido justamente para não sobreajustar
# um conjunto dourado pequeno. Ao mexer nesses números, rode rag/bench.sh.
RRF_K = 8
W_DENSE = 1.0
W_LEX = 1.0 # multiplicado pela similaridade bruta do casamento
W_SUMMARY = 1.5 # o resumo é curto e preciso: um acerto ali vale mais
# Piso do casamento lexical. Calibrado por medição: com os probes abaixo, um
# nome de tipo procurado diretamente pontua 1.00 e o segundo colocado no
# máximo 0.95, enquanto NENHUMA consulta conceitual em português passa de
# 0.35. Ou seja, 0.5 faz o lado lexical se calar quando não tem nada a dizer,
# em vez de afogar a lista densa com ruído.
LEX_MIN = 0.5
# Quantos chunks do mesmo arquivo podem ocupar o top-k. Sem esse limite um
# arquivo grande toma todos os lugares: numa medição o WizardModel.swift
# ficou com 3 dos 5 resultados, gastando token sem trazer nada novo.
MAX_PER_FILE = 2
# Quanto código o modo --snippet mostra por resultado.
SNIPPET_CHARS = 300
# Colunas extras introduzidas pela migration 002. O banco legado `doza` não
# as tem — este mesmo código atende os dois, então descobrimos quais existem
# e substituímos as ausentes por NULL, em vez de quebrar a busca do legado.
_EXTRA_COLS = ("start_line", "end_line", "symbols", "module", "kind")
_cols_cache = {}
def _available_cols(cur, schema):
if schema not in _cols_cache:
cur.execute(
"""SELECT column_name FROM information_schema.columns
WHERE table_schema = %s AND table_name = 'code_chunks'""",
(schema,),
)
_cols_cache[schema] = {r[0] for r in cur.fetchall()}
return _cols_cache[schema]
def _select_cols(cur, schema):
have = _available_cols(cur, schema)
extra = ", ".join(f"c.{c}" if c in have else f"NULL AS {c}"
for c in _EXTRA_COLS)
return f"c.id, c.file_path, c.content, {extra}"
def _db_connect():
return psycopg2.connect(
host=os.environ.get("RAG_DB_HOST", "127.0.0.1"),
port=os.environ.get("RAG_DB_PORT", "5433"),
dbname=os.environ.get("RAG_DB_NAME", "rag_doza"),
user=os.environ.get("RAG_DB_USER", "rag_doza_indexer"),
password=os.environ["RAG_DB_PASSWORD"],
connect_timeout=5,
)
def _schema():
return os.environ.get("RAG_DB_SCHEMA", "doza")
def _qid(schema):
"""Schema como identificador SQL seguro (aspas duplas) — aceita guífen
(ex: `jhonny-rag`) sem quebrar a sintaxe. Retroativo: produtos sem guífen
voltam idênticos (`doza` -> `"doza"`)."""
return '"' + schema.replace('"', '""') + '"'
def _filters(module, path, ext, have=()):
"""Cláusulas de escopo aplicadas antes do ranqueamento."""
clauses, params = [], []
if module and "module" in have:
clauses.append("c.module = %s")
params.append(module)
if path:
clauses.append("c.file_path ILIKE %s")
params.append(f"%{path}%")
if ext:
clauses.append("c.file_path LIKE %s")
params.append(f"%{ext}")
return (" AND " + " AND ".join(clauses) if clauses else ""), params
def _probe_terms(query):
"""Termos que o lado lexical tenta casar contra os símbolos.
Só dois: a consulta inteira e a versão sem espaços — é esta que faz
"silence cut pipeline" casar 1.00 com `SilenceCutPipeline`.
Palavra a palavra NÃO funciona, por mais tentador que pareça: termos
portugueses comuns casam com qualquer lista de símbolos ("fora" pontuou
1.00 contra `WizardStep`, "forma" 0.83 contra meia base) e o ruído
expulsava do top-5 acertos legítimos do lado denso — o `FacePerceiver`
era o 1º da lista densa para "detectar rostos" e sumia do resultado.
"""
terms = [query, query.replace(" ", "")]
return list(dict.fromkeys(t for t in terms if t))
def _row_to_dict(row):
return {
"id": row[0], "file_path": row[1], "content": row[2],
"start_line": row[3], "end_line": row[4],
"symbols": row[5], "module": row[6], "kind": row[7],
}
def _dense(cur, schema, q_emb, limit, where, params):
# O HNSW só explora `ef_search` candidatos; 64 dá folga sobre um top-k
# pequeno sem custar latência perceptível nesta escala.
cur.execute("SET LOCAL hnsw.ef_search = 64")
cur.execute(
f"""
SELECT {_select_cols(cur, schema)}, 1 - (c.embedding <=> %s::vector) AS s
FROM {_qid(schema)}.code_chunks c
WHERE c.embedding IS NOT NULL {where}
ORDER BY c.embedding <=> %s::vector
LIMIT %s
""",
[q_emb] + params + [q_emb, limit],
)
return [(_row_to_dict(r), float(r[8])) for r in cur.fetchall()]
def _lexical(cur, schema, terms, limit, where, params):
# Casa contra `symbols` (nome do arquivo + tipo + membros declarados),
# nunca contra o caminho inteiro: nomes de diretório como `Pipeline/`
# casariam com meia base e afogariam o sinal.
have = _available_cols(cur, schema)
stem = "regexp_replace(c.file_path, '^.*/|\\.[^.]*$', '', 'g')"
target = f"coalesce(c.symbols, {stem})" if "symbols" in have else stem
# Desempate estável: pela linha inicial onde ela existe, senão pelo id.
tiebreak = "c.start_line" if "start_line" in have else "c.id"
score = f"(SELECT max(word_similarity(t, {target})) FROM unnest(%s::text[]) t)"
cur.execute(
f"""
SELECT {_select_cols(cur, schema)}, {score} AS s
FROM {_qid(schema)}.code_chunks c
WHERE {score} >= %s {where}
ORDER BY s DESC, {tiebreak} ASC
LIMIT %s
""",
[terms] + [terms, LEX_MIN] + params + [limit],
)
return [(_row_to_dict(r), float(r[8])) for r in cur.fetchall()]
def _summary_dense(cur, schema, q_emb, limit, where, params):
"""Terceira lista: busca sobre o resumo do arquivo, não sobre o código.
Devolve o melhor trecho de cada arquivo cujo resumo casa com a consulta.
Existe porque a lista por trecho é dominada por arquivos grandes — eles
casam morno com tudo e empurram para baixo arquivos pequenos e exatos.
"""
cur.execute("SELECT to_regclass(%s)", (f"{_qid(schema)}.file_index",))
if cur.fetchone()[0] is None:
return []
cur.execute("SET LOCAL hnsw.ef_search = 64")
cur.execute(
f"""
SELECT file_path, 1 - (summary_embedding <=> %s::vector) AS s
FROM {_qid(schema)}.file_index
WHERE summary_embedding IS NOT NULL
ORDER BY summary_embedding <=> %s::vector
LIMIT %s
""",
(q_emb, q_emb, limit),
)
hits = cur.fetchall()
if not hits:
return []
order = {fp: i for i, (fp, _) in enumerate(hits)}
raw = {fp: s for fp, s in hits}
# Para cada arquivo achado pelo resumo, o trecho mais próximo da consulta.
cur.execute(
f"""
SELECT DISTINCT ON (c.file_path) {_select_cols(cur, schema)}
FROM {_qid(schema)}.code_chunks c
WHERE c.file_path = ANY(%s) AND c.embedding IS NOT NULL {where}
ORDER BY c.file_path, c.embedding <=> %s::vector
""",
[list(order)] + params + [q_emb],
)
rows = [_row_to_dict(r) for r in cur.fetchall()]
rows.sort(key=lambda r: order[r["file_path"]])
return [(r, raw[r["file_path"]]) for r in rows]
def _fuse(ranked_lists):
"""Reciprocal Rank Fusion ponderada sobre listas já ordenadas.
RRF puro só olha a posição, e isso empatava o 1º lugar lexical com o 1º
dense — deixando de fora casos em que só o lado lexical acertava
(`AtomicFileIO` para "gravar arquivo de forma atomica"). Por isso cada
lista traz uma função de peso: a lexical escala com a similaridade
bruta, de modo que um casamento quase exato de nome vence o empate.
"""
scores, best = {}, {}
for results, weight_of in ranked_lists:
for rank, (row, raw) in enumerate(results, 1):
contrib = weight_of(raw) / (RRF_K + rank)
scores[row["id"]] = scores.get(row["id"], 0.0) + contrib
best.setdefault(row["id"], row)
ordered = sorted(scores.items(), key=lambda kv: -kv[1])
return [dict(best[i], score=s) for i, s in ordered]
def _dedupe(rows, top_k, max_per_file=MAX_PER_FILE):
"""Limita chunks por arquivo; o excedente vira uma nota de localização."""
kept, counts, extras = [], {}, {}
for row in rows:
fp = row["file_path"]
if counts.get(fp, 0) < max_per_file:
counts[fp] = counts.get(fp, 0) + 1
row["also_at"] = []
kept.append(row)
else:
extras.setdefault(fp, []).append((row["start_line"], row["end_line"]))
for row in kept:
row["also_at"] = extras.get(row["file_path"], [])[:3]
return kept[:top_k]
def _attach_map(cur, schema, rows):
"""Anexa tipo principal e resumo de `file_index`, quando existir."""
cur.execute("SELECT to_regclass(%s)", (f"{_qid(schema)}.file_index",))
if cur.fetchone()[0] is None or not rows:
return rows
paths = list({r["file_path"] for r in rows})
cur.execute(
f"SELECT file_path, main_type, summary FROM {_qid(schema)}.file_index "
f"WHERE file_path = ANY(%s)",
(paths,),
)
info = {p: (t, s) for p, t, s in cur.fetchall()}
for row in rows:
main_type, summary = info.get(row["file_path"], (None, None))
row["main_type"] = main_type
row["summary"] = summary
return rows
def rag_search(query, top_k=5, module=None, path=None, ext=None, dense_only=False):
"""Busca híbrida. Devolve dicts com file_path, faixa de linhas e score."""
schema = _schema()
# Ampliamos o pool antes de fundir e deduplicar: o item certo pode estar
# em 8º numa lista e em 1º na outra, e a fusão é que o traz para cima.
pool = max(top_k * 4, 20)
conn = _db_connect()
try:
with conn.cursor() as cur:
where, params = _filters(module, path, ext,
_available_cols(cur, schema))
q_emb = _embed(query, prefix=QUERY_PREFIX)
lists = [(_dense(cur, schema, q_emb, pool, where, params),
lambda raw: W_DENSE)]
if not dense_only:
lists.append((_lexical(cur, schema, _probe_terms(query),
pool, where, params),
lambda raw: W_LEX * (0.5 + raw)))
lists.append((_summary_dense(cur, schema, q_emb, pool,
where, params),
lambda raw: W_SUMMARY))
rows = _dedupe(_fuse(lists), top_k)
rows = _attach_map(cur, schema, rows)
conn.commit()
finally:
conn.close()
return rows
def map_files(term=None, module=None, limit=60):
"""Inventário de arquivos — responde "onde fica X" sem corpo de código."""
schema = _schema()
conn = _db_connect()
try:
with conn.cursor() as cur:
cur.execute("SELECT to_regclass(%s)", (f"{_qid(schema)}.file_index",))
if cur.fetchone()[0] is None:
return []
clauses, params = [], []
if module:
clauses.append("module = %s")
params.append(module)
if term:
clauses.append("(file_path ILIKE %s OR main_type ILIKE %s "
"OR summary ILIKE %s)")
params += [f"%{term}%"] * 3
where = "WHERE " + " AND ".join(clauses) if clauses else ""
cur.execute(
f"""SELECT file_path, module, main_type, summary, n_lines
FROM {_qid(schema)}.file_index {where}
ORDER BY module, file_path LIMIT %s""",
params + [limit],
)
return [
{"file_path": r[0], "module": r[1], "main_type": r[2],
"summary": r[3], "n_lines": r[4]}
for r in cur.fetchall()
]
finally:
conn.close()
# ---------------------------------------------------------------------------
# Formatação
# ---------------------------------------------------------------------------
# Linhas que não descrevem nada: delimitadores de docstring, cercas e
# separadores. Aparecem no topo de arquivos Python do índice legado.
_NOISE = {'"""', "'''", "# ---", "---", "/*", "*/", "{", "}", "*"}
def _first_doc_line(content):
"""Primeira linha que serve de descrição do trecho.
Usada quando o arquivo não tem resumo em `file_index` — caso do banco
legado, que não passou pela migration 002.
"""
for line in content.splitlines():
s = line.strip()
if s.startswith(("///", "//", "#", "*")):
s = s.lstrip("/#* ").strip()
if s and s not in _NOISE:
return s
for line in content.splitlines():
s = line.strip().strip("\"'").strip()
if s and s not in _NOISE and not s.startswith("import"):
return s
return ""
def format_results(results, mode="map"):
"""Renderiza o resultado no modo pedido.
O padrão é `map`: caminho + faixa de linhas + uma linha de descrição, sem
nenhum corpo de código. É o modo mais barato e quase sempre suficiente —
com a faixa em mãos o agente lê exatamente a janela que precisa.
"""
if not results:
return "[RAG vazio, buscando local]\n"
if mode == "json":
return json.dumps(results, ensure_ascii=False, indent=2) + "\n"
out = []
for i, r in enumerate(results, 1):
# O banco legado não tem faixa de linhas; sem ela, mostramos só o
# caminho em vez de um ":None-None" que não serve para nada.
loc = r["file_path"]
if r.get("start_line") and r.get("end_line"):
loc += f":{r['start_line']}-{r['end_line']}"
out.append(f"{i} {r['score']:.2f} {loc}")
desc = r.get("summary") or _first_doc_line(r["content"])
# A regra "uma classe por arquivo" faz o tipo principal repetir o
# nome do arquivo quase sempre; imprimir os dois é token jogado fora.
stem = os.path.splitext(os.path.basename(r["file_path"]))[0]
label = r.get("main_type") or ""
if label == stem:
label = ""
if label and desc:
out.append(f" {label} · {desc[:78]}")
elif desc:
out.append(f" {desc[:88]}")
spans = [f"{a}-{b}" for a, b in r.get("also_at", []) if a and b]
if spans:
out.append(f" (+ tambem em {', '.join(spans)})")
if mode == "snippet":
# Corta em fronteira de LINHA: um trecho de código partido no
# meio de um identificador não ajuda ninguém a decidir se vale
# abrir o arquivo.
body, size = [], 0
for ln in r["content"].splitlines():
if body and size + len(ln) > SNIPPET_CHARS:
body.append("…")
break
body.append(ln)
size += len(ln) + 1
out.append("".join(f" | {ln}\n" for ln in body))
elif mode == "full":
out.append("".join(f" | {ln}\n" for ln in r["content"].splitlines()))
return "\n".join(out) + "\n"
def format_map(rows):
if not rows:
return "[RAG vazio, buscando local]\n"
out = []
current = None
for r in rows:
if r["module"] != current:
current = r["module"]
out.append(f"\n{current or '(sem modulo)'}")
name = os.path.basename(r["file_path"])
desc = (r["summary"] or "")[:78]
out.append(f" {name:<38} {r['n_lines']:>5}L {desc}")
return "\n".join(out) + "\n"
def main():
ap = argparse.ArgumentParser(
description="Busca RAG hibrida (densa + lexical, fundidas com RRF)")
ap.add_argument("query", nargs="?", help="consulta")
ap.add_argument("top_k", nargs="?", type=int, default=5)
ap.add_argument("--snippet", action="store_true", help="mostra 300 chars do trecho")
ap.add_argument("--full", action="store_true", help="mostra o trecho inteiro")
ap.add_argument("--json", action="store_true", help="saida estruturada")
ap.add_argument("--module", help="restringe a um modulo (ex: TigreAI)")
ap.add_argument("--path", help="restringe a caminhos contendo este texto")
ap.add_argument("--ext", help="restringe a uma extensao (ex: .swift)")
ap.add_argument("--map", dest="map_term", nargs="?", const="",
help="inventario de arquivos em vez de busca por trecho")
ap.add_argument("--dense-only", action="store_true",
help="desliga o lado lexical (para comparacao)")
args = ap.parse_args()
if args.map_term is not None:
print(format_map(map_files(term=args.map_term or None, module=args.module)))
return
if not args.query:
ap.error("informe a consulta, ou use --map")
mode = ("json" if args.json else "full" if args.full
else "snippet" if args.snippet else "map")
results = rag_search(args.query, args.top_k, module=args.module,
path=args.path, ext=args.ext, dense_only=args.dense_only)
print(format_results(results, mode=mode))
if __name__ == "__main__":
main()
+10
View File
@@ -0,0 +1,10 @@
#!/bin/bash
# Busca semantica no banco RAG do sistema legado (code/).
# Uso: rag/search_doza.sh "consulta" [top_k] [--snippet|--full|--json|--map]
set -e
RAG_DIR="$(cd "$(dirname "$0")" && pwd)"
"$RAG_DIR/ensure_tunnel.sh" >&2
PY="${RAG_DIR}/.venv/bin/python3"
[ -x "$PY" ] || PY="$(command -v python3)"
export RAG_ENV_FILE="$RAG_DIR/.env"
exec "$PY" "$RAG_DIR/search.py" "$@"
+10
View File
@@ -0,0 +1,10 @@
#!/bin/bash
# Busca semantica no banco RAG do projeto Jhonny (code/).
# Uso: rag/search_jhonny.sh "consulta" [top_k] [--snippet|--full|--json|--map]
set -e
RAG_DIR="$(cd "$(dirname "$0")" && pwd)"
"$RAG_DIR/ensure_tunnel.sh" >&2
PY="${RAG_DIR}/.venv/bin/python3"
[ -x "$PY" ] || PY="$(command -v python3)"
export RAG_ENV_FILE="$RAG_DIR/jhonny.env"
exec "$PY" "$RAG_DIR/search.py" "$@"
+16
View File
@@ -0,0 +1,16 @@
#!/bin/bash
# Busca semântica no banco RAG do sistema Tigre (codeclass/Sources).
# Uso: rag/search_tigre.sh "consulta" [top_k]
set -e
RAG_DIR="$(cd "$(dirname "$0")" && pwd)"
"$RAG_DIR/ensure_tunnel.sh" >&2
PY="${RAG_DIR}/.venv/bin/python3"
if [ ! -x "$PY" ]; then
PY="$(command -v python3)"
fi
export RAG_ENV_FILE="$RAG_DIR/tigre.env"
exec "$PY" "$RAG_DIR/search.py" "$@"
+262
View File
@@ -0,0 +1,262 @@
"""Corte de arquivos Swift em trechos por declaração, com faixa de linhas.
O indexador antigo cortava em janelas cegas de 60 linhas, o que partia
funções ao meio e não dizia em que linha cada trecho começava. Aqui o corte
segue a estrutura do código: o cabeçalho do arquivo (imports + doc do tipo)
vira um trecho, e cada membro de topo do tipo vira outro, já com a faixa de
linhas que o agente usa para ler só aquela janela.
Combina com a regra do projeto de UMA CLASSE POR ARQUIVO (ver
codeclass/README.md): o "tipo principal" de um arquivo é o primeiro tipo
público declarado nele.
"""
import os
import re
# Palavras que abrem uma declaração. `case` entra por causa de enums, que
# neste projeto carregam doc-comment por caso.
_DECL_KW = (
"func|init|deinit|subscript|var|let|case|enum|struct|class|protocol"
"|actor|typealias|extension"
)
# Modificadores que podem preceder a palavra-chave da declaração.
_MODIFIERS = (
"public|internal|private|fileprivate|open|final|static|class|override"
"|mutating|nonmutating|required|convenience|lazy|weak|unowned|indirect"
"|nonisolated|isolated|dynamic|optional|async|throws"
)
_DECL_RE = re.compile(
rf"^(?P<indent>[ \t]*)(?:(?:{_MODIFIERS})\s+)*(?P<kw>{_DECL_KW})\b"
rf"(?:\s+(?P<name>[A-Za-z_][A-Za-z0-9_]*))?"
)
# Tipos declarados no nível 0 do arquivo.
_TYPE_RE = re.compile(
rf"^(?:(?:{_MODIFIERS})\s+)*(?P<kw>enum|struct|class|protocol|actor|extension)"
rf"\s+(?P<name>[A-Za-z_][A-Za-z0-9_]*)"
)
_PUBLIC_RE = re.compile(r"^\s*(?:public|open)\b")
# Nomes declarados em um trecho — alimenta a coluna `symbols`, que é o lado
# lexical (pg_trgm) da busca híbrida.
_SYMBOL_RE = re.compile(
rf"\b(?:{_DECL_KW})\s+(?P<name>[A-Za-z_][A-Za-z0-9_]*)"
)
# Linhas que não abrem nem fecham escopo de verdade, para a contagem de
# chaves não ser enganada por comentário e string literal.
_LINE_COMMENT_RE = re.compile(r"//.*$")
_STRING_RE = re.compile(r'"(?:[^"\\]|\\.)*"')
# Arquivos até este tamanho viram um único trecho: o arquivo inteiro. A
# maioria dos arquivos do projeto cai aqui (média ~70 linhas), e um trecho
# igual ao arquivo é o melhor resultado possível de busca.
WHOLE_FILE_MAX_LINES = 120
# Teto rígido de caracteres por trecho. O nomic-embed-text tem janela de
# 2048 tokens (~4 chars/token); ficamos com folga. Quando um trecho passa
# disso ele é dividido POR LINHA (nunca no meio de uma linha), para
# start_line/end_line continuarem verdadeiros.
MAX_CHARS = 5000
# Tamanho que um trecho tenta alcançar juntando declarações vizinhas. Sem
# isso cada `var` de uma linha viraria um trecho próprio: mais embeddings
# para gerar, e cada resultado de busca carregando pouco contexto útil.
TARGET_CHARS = 2000
def _strip_noise(line):
return _LINE_COMMENT_RE.sub("", _STRING_RE.sub('""', line))
def _preamble_start(lines, i):
"""Recua de `i` para incluir doc-comment e atributos da declaração."""
j = i
while j > 0:
prev = lines[j - 1].strip()
if prev.startswith("///") or prev.startswith("//") or prev.startswith("@") \
or prev.startswith("*") or prev.startswith("/*"):
j -= 1
else:
break
return j
def _split_long(chunk_lines, start_line, max_chars=MAX_CHARS):
"""Divide um trecho grande em pedaços, sempre em fronteira de linha."""
out = []
buf, buf_start = [], start_line
size = 0
for offset, line in enumerate(chunk_lines):
if buf and size + len(line) + 1 > max_chars:
out.append((buf, buf_start))
buf, buf_start, size = [], start_line + offset, 0
buf.append(line)
size += len(line) + 1
if buf:
out.append((buf, buf_start))
return out
def _merge_small(chunks):
"""Junta declarações vizinhas curtas até `TARGET_CHARS`.
Só funde trechos contíguos (o fim de um encosta no começo do outro), para
a faixa de linhas do trecho fundido continuar sendo uma janela única e
legível com `Read(offset=…, limit=…)`.
"""
merged = []
for c in chunks:
prev = merged[-1] if merged else None
contiguous = prev is not None and prev["end_line"] + 1 == c["start_line"]
same_kind = prev is not None and prev["kind"] == c["kind"] == "decl"
fits = prev is not None and \
len(prev["content"]) + len(c["content"]) + 1 <= TARGET_CHARS
if contiguous and same_kind and fits:
prev["content"] += "\n" + c["content"]
prev["end_line"] = c["end_line"]
prev["symbols"] = " ".join(
sorted(set(prev["symbols"].split()) | set(c["symbols"].split()))
)
else:
merged.append(dict(c))
return merged
def _symbols_of(text, file_stem, main_type):
"""Nomes buscáveis do trecho.
O nome do arquivo entra sempre: é o que faz uma consulta pelo nome do
tipo (`FCPXMLValidator`) casar com o arquivo certo — exatamente o caso
em que a busca puramente densa errava.
"""
names = {file_stem}
if main_type:
names.add(main_type)
names.update(m.group("name") for m in _SYMBOL_RE.finditer(text))
return " ".join(sorted(n for n in names if n))
def file_facts(text, rel_path):
"""Tipo principal, símbolos públicos e resumo — alimenta `file_index`."""
lines = text.splitlines()
main_type = None
summary = None
public_symbols = []
for i, line in enumerate(lines):
m = _TYPE_RE.match(line)
if m and main_type is None and m.group("kw") != "extension":
main_type = m.group("name")
# Resumo: primeira linha do doc-comment logo acima do tipo.
for k in range(_preamble_start(lines, i), i):
doc = lines[k].strip()
if doc.startswith("///"):
summary = doc.lstrip("/").strip()
break
if _PUBLIC_RE.match(line):
d = _DECL_RE.match(line)
if d and d.group("name"):
public_symbols.append(d.group("name"))
if main_type is None:
main_type = os.path.splitext(os.path.basename(rel_path))[0]
if summary is None:
for line in lines[:40]:
s = line.strip()
if s.startswith("///"):
summary = s.lstrip("/").strip()
break
return {
"main_type": main_type,
"summary": summary,
"public_symbols": sorted(set(public_symbols)),
"n_lines": len(lines),
}
def chunk_swift(text, rel_path):
"""Divide um arquivo Swift em trechos com faixa de linhas e símbolos.
Devolve lista de dicts: content, start_line, end_line, symbols, kind.
Linhas são 1-indexadas e inclusivas nas duas pontas, iguais ao que o
`Read(offset=…, limit=…)` do agente espera.
"""
lines = text.splitlines()
if not lines:
return []
stem = os.path.splitext(os.path.basename(rel_path))[0]
facts = file_facts(text, rel_path)
main_type = facts["main_type"]
def build(chunk_lines, start_line, kind):
out = []
for piece, piece_start in _split_long(chunk_lines, start_line):
body = "\n".join(piece).strip()
if not body:
continue
out.append({
"content": body,
"start_line": piece_start,
"end_line": piece_start + len(piece) - 1,
"symbols": _symbols_of(body, stem, main_type),
"kind": kind,
})
return out
# Arquivo curto: um trecho só, o arquivo inteiro. É o resultado de busca
# ideal — nada de emendar pedaços depois.
if len(lines) <= WHOLE_FILE_MAX_LINES:
return build(lines, 1, "file")
# Acha onde o corpo do tipo principal abre (primeira ida de 0 para 1 na
# contagem de chaves) para separar o cabeçalho do arquivo.
depth = 0
body_start = None
depths = []
for i, line in enumerate(lines):
clean = _strip_noise(line)
opened = clean.count("{")
closed = clean.count("}")
depths.append(depth) # profundidade NO INÍCIO da linha
if body_start is None and depth == 0 and opened > closed:
body_start = i + 1
depth += opened - closed
if body_start is None:
# Sem corpo de tipo reconhecível (arquivo só de typealias, por ex).
return build(lines, 1, "file")
chunks = build(lines[:body_start], 1, "header")
# Membros: declarações que começam com o corpo do tipo aberto (nível 1).
starts = []
for i in range(body_start, len(lines)):
if depths[i] != 1:
continue
if not _DECL_RE.match(lines[i]):
continue
s = _preamble_start(lines, i)
if s >= body_start and (not starts or s > starts[-1]):
starts.append(s)
if not starts:
chunks.extend(build(lines[body_start:], body_start + 1, "window"))
return chunks
# O que sobra entre o cabeçalho e o primeiro membro (propriedades soltas,
# por exemplo) é anexado ao cabeçalho como trecho próprio.
if starts[0] > body_start:
chunks.extend(build(lines[body_start:starts[0]], body_start + 1, "decl"))
for idx, s in enumerate(starts):
e = starts[idx + 1] if idx + 1 < len(starts) else len(lines)
chunks.extend(build(lines[s:e], s + 1, "decl"))
return _merge_small(chunks)
+10
View File
@@ -0,0 +1,10 @@
RAG_DB_HOST=127.0.0.1
RAG_DB_PORT=55435
RAG_DB_NAME=rag_tigre
RAG_DB_USER=rag_tigre_indexer
RAG_DB_PASSWORD=TghUpFOJGazA9EJyCHbuNEfs_OC5hH4d
RAG_DB_SCHEMA=tigre
RAG_TARGET_DIR=codeclass/Sources
RAG_ENV_FILE=tigre.env
RAG_EMBED_PREFIX=1
+310
View File
@@ -0,0 +1,310 @@
"""Corte de archivos TypeScript/JavaScript en trechos por declaración.
Hermano del `swift_chunker` y `python_chunker`, para el proyecto Jhonny
(`code/`), que es MCP en TypeScript. Antes estos archivos caían en el
cortador genérico por ventana de líneas, que graba `symbols: None` — y eso
deja vacía la columna que alimenta el lado lexical (pg_trgm) de la busca
híbrida. El resultado: buscar `OAuthResourceServer` no rescata ningún
`.ts`, porque el único señal que quedaba era el denso (embedding).
Aquí el corte sigue la estructura del archivo: el preámbulo (imports + doc
de la primera declaración exportada) vira un trecho, y cada declaración de
topo (`export class/interface/type/function/const`, o sus variantes sin
`export`) vira otro. Los identificadores exportados entran en `symbols` con
la misma faixa de `start_line`/`end_line` que el agente usa para leer sólo
esa ventana.
Tipos de declaración soportados (patrones reales vistos en code/src/):
export function buildContentSecurityPolicy(...): string
export interface AuthenticatedPrincipal { ... }
export type AuthenticationResult = A | B
export const HTTP_SECURITY_HEADERS = Object.freeze({ ... })
export class OAuthResourceServer { ... }
function bearerToken(...) // sin export sigue importando
type JwtVerifier = (...) => Promise<...>
"""
import os
import re
# Palabras que abren una declaración a nivel de tope. `type` entra por los
# alias de tipo (`export type X = ...`); `const`/`let`/`var` por las
# constantes exportadas (ej: `export const HTTP_SECURITY_HEADERS`).
_DECL_KW = "class|interface|type|enum|function|const|let|var"
# Modificadores que preceden la palabra clave de la declaración: `export`
# primero, y después `default`/`async`/`abstract`/`declare`.
_MODIFIERS = "export|default|async|abstract|declare|global"
_DECL_RE = re.compile(
rf"^(?P<indent>[ \t]*)(?:(?:{_MODIFIERS})\s+)*(?P<kw>{_DECL_KW})"
rf"\s+(?P<name>[A-Za-z_$][A-Za-z0-9_$]*)"
)
# Declaración exportada con nombre — alimenta `public_symbols` y el tipo
# principal del archivo (`file_facts`).
_EXPORT_RE = re.compile(
rf"^(?:export\s+)(?:(?:{_MODIFIERS})\s+)*(?P<kw>{_DECL_KW})"
rf"\s+(?P<name>[A-Za-z_$][A-Za-z0-9_$]*)"
)
# Declaraciones que vuelven `main_type` del archivo (tipos, no funciones
# ni constantes). Sin `export` se ignora: un helper interno no manda.
_TYPE_RE = re.compile(
rf"^(?:export\s+)(?:(?:{_MODIFIERS})\s+)*(?P<kw>class|interface|type|enum)"
rf"\s+(?P<name>[A-Za-z_$][A-Za-z0-9_$]*)"
)
# Nombres declarados en un trecho — alimenta la columna `symbols` (lado
# lexical pg_trgm). Casa cualquier `kw name` del cuerpo, incluidos métodos
# y constantes internas de una clase, que también son nombres buscables.
_SYMBOL_RE = re.compile(
rf"\b(?:{_DECL_KW})\s+(?P<name>[A-Za-z_$][A-Za-z0-9_$]*)"
)
# Líneas que no abren ni cierran escopo de verdad, para que la contada de
# llaves no se engañe con comentario o literal. TS usa comillas simples,
# dobles y backticks (template literals): cubrimos las tres.
_LINE_COMMENT_RE = re.compile(r"//.*$")
_STRING_RE = re.compile(
r'"(?:[^"\\]|\\.)*"|\'(?:[^\'\\]|\\.)*\'|`(?:[^`\\]|\\.)*`'
)
# Archivos hasta este tamaño vienen un único trecho: el archivo entero.
WHOLE_FILE_MAX_LINES = 120
# Teto rígido de caracteres por trecho (nomic-embed-text: 2048 tokens).
MAX_CHARS = 5000
# Tamaño que un trecho intenta alcanzar juntando declaraciones vecinas.
TARGET_CHARS = 2000
def _strip_noise(line):
return _LINE_COMMENT_RE.sub("", _STRING_RE.sub('""', line))
def _preamble_start(lines, i):
"""Recua de `i` para incluir el doc-comment y anotaciones de la declaración."""
j = i
while j > 0:
prev = lines[j - 1].strip()
if prev.startswith("//") or prev.startswith("*") or prev.startswith("/*") \
or prev.startswith("@"):
j -= 1
else:
break
return j
def _split_long(chunk_lines, start_line, max_chars=MAX_CHARS):
"""Divide un trecho grande en pedazos, siempre en frontera de línea."""
out, buf, buf_start, size = [], [], start_line, 0
for offset, line in enumerate(chunk_lines):
if buf and size + len(line) + 1 > max_chars:
out.append((buf, buf_start))
buf, buf_start, size = [], start_line + offset, 0
buf.append(line)
size += len(line) + 1
if buf:
out.append((buf, buf_start))
return out
def _merge_small(chunks):
"""Junta declaraciones vecinas cortas hasta `TARGET_CHARS`.
Sólo funde trechos contiguos (el fin de uno encosta el comienzo de otro),
para que la faixa de líneas del trecho fundido siga siendo una ventana
única y legible con `Read(offset=…, limit=…)`.
"""
merged = []
for c in chunks:
prev = merged[-1] if merged else None
contiguous = prev is not None and prev["end_line"] + 1 == c["start_line"]
same_kind = prev is not None and prev["kind"] == c["kind"] == "decl"
fits = prev is not None and \
len(prev["content"]) + len(c["content"]) + 1 <= TARGET_CHARS
if contiguous and same_kind and fits:
prev["content"] += "\n" + c["content"]
prev["end_line"] = c["end_line"]
prev["symbols"] = " ".join(
sorted(set(prev["symbols"].split()) | set(c["symbols"].split()))
)
else:
merged.append(dict(c))
return merged
def _symbols_of(text, file_stem, main_type):
"""Nombres buscables del trecho.
El nombre del archivo entra siempre, igual que el tipo principal: es lo
que hace que una consulta por el nombre (`OAuthResourceServer`) case con
el archivo derecho — exactamente el caso en que la busca densa erraba.
"""
names = {file_stem}
if main_type:
names.add(main_type)
names.update(m.group("name") for m in _SYMBOL_RE.finditer(text))
return " ".join(sorted(n for n in names if n))
def file_facts(text, rel_path):
"""Tipo principal, símbolos públicos y resumen — alimenta `file_index`."""
lines = text.splitlines()
main_type = None
summary = None
public_symbols = []
for i, line in enumerate(lines):
m = _EXPORT_RE.match(line)
if m and m.group("name"):
public_symbols.append(m.group("name"))
tm = _TYPE_RE.match(line)
if tm and main_type is None:
main_type = tm.group("name")
# Resumen: primera línea del doc-comment justo encima del tipo.
for k in range(_preamble_start(lines, i), i):
doc = lines[k].strip()
if doc.startswith("//") or doc.startswith("*"):
summary = doc.lstrip("/").strip().lstrip("*").strip()
break
if main_type is None:
main_type = os.path.splitext(os.path.basename(rel_path))[0]
if summary is None:
for line in lines[:40]:
s = line.strip()
if s.startswith("//"):
summary = s.lstrip("/").strip()
break
return {
"main_type": main_type,
"summary": summary,
"public_symbols": sorted(set(public_symbols)),
"n_lines": len(lines),
}
def _depth_at_line_starts(lines):
"""Profundidad de llaves al INICIO de cada línea."""
depth = 0
depths = []
for line in lines:
clean = _strip_noise(line)
depths.append(depth)
depth += clean.count("{") - clean.count("}")
return depths
def _top_decls(lines):
"""Declaraciones de tope: lista de (inicio, fin) en índices medio-abiertos.
Una declaración empieza en una línea a profundidad 0 que casa `_DECL_RE` e
incluye su preámbulo (doc-comment/anotaciones); va hasta la próxima
declaración de tope o el final del archivo.
"""
depths = _depth_at_line_starts(lines)
starts = []
for i, line in enumerate(lines):
if depths[i] == 0 and _DECL_RE.match(line):
starts.append(_preamble_start(lines, i))
# Normaliza solapamientos (un preámbulo puede meter la declaración previa).
cleaned = []
for s in starts:
if not cleaned or s > cleaned[-1]:
cleaned.append(s)
starts = cleaned
out = []
for idx, s in enumerate(starts):
e = starts[idx + 1] if idx + 1 < len(starts) else len(lines)
out.append((s, e))
return out
def _body_open(lines, s, e):
"""Índice de la línea donde se abre el cuerpo `{` de una declaración de
tope (primera ida de 0 a 1 dentro de [s, e)). None si no abre cuerpo."""
depth = 0
for i in range(s, e):
clean = _strip_noise(lines[i])
opened = clean.count("{")
closed = clean.count("}")
if depth == 0 and opened > closed:
return i
depth += opened - closed
return None
def _member_starts(lines, body_start, decl_end, depths):
"""Miembros directos (nivel 1) del cuerpo de una clase/interface/enum."""
starts = []
for i in range(body_start + 1, decl_end):
if depths[i] != 1:
continue
if not _DECL_RE.match(lines[i]):
continue
s = _preamble_start(lines, i)
if s > body_start and (not starts or s > starts[-1]):
starts.append(s)
return starts
def chunk_ts(text, rel_path):
"""Divide un archivo TS/JS en trechos con faixa de líneas y símbolos.
Devuelve lista de dicts: content, start_line, end_line, symbols, kind.
Las líneas son 1-indexadas e inclusivas en las dos puntas.
"""
lines = text.splitlines()
if not lines:
return []
stem = os.path.splitext(os.path.basename(rel_path))[0]
main_type = file_facts(text, rel_path)["main_type"]
def build(start, end, kind):
out = []
for piece, piece_start in _split_long(lines[start:end], start + 1):
body = "\n".join(piece).strip()
if not body:
continue
out.append({
"content": body,
"start_line": piece_start,
"end_line": piece_start + len(piece) - 1,
"symbols": _symbols_of(body, stem, main_type),
"kind": kind,
})
return out
if len(lines) <= WHOLE_FILE_MAX_LINES:
return build(0, len(lines), "file")
top = _top_decls(lines)
if not top:
return build(0, len(lines), "window")
depths = _depth_at_line_starts(lines)
chunks = build(0, top[0][0], "header") if top[0][0] > 0 else []
for s, e in top:
size = sum(len(lines[i]) + 1 for i in range(s, e))
if size <= TARGET_CHARS:
chunks.extend(build(s, e, "decl"))
continue
# Tipo grande (clase/interface con muchos miembros): corta el cuerpo
# por miembro (nivel 1), manteniendo la firma del tipo en el primero.
body_open = _body_open(lines, s, e)
if body_open is None:
chunks.extend(build(s, e, "decl"))
continue
members = _member_starts(lines, body_open, e, depths)
if not members:
chunks.extend(build(s, e, "decl"))
continue
chunks.extend(build(s, members[0], "decl"))
for idx, si in enumerate(members):
ei = members[idx + 1] if idx + 1 < len(members) else e
chunks.extend(build(si, ei, "decl"))
return _merge_small(chunks)