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:
@@ -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
@@ -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).
|
||||
@@ -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).
|
||||
@@ -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"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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": []
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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" "$@"
|
||||
Executable
+9
@@ -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" "$@"
|
||||
Executable
+9
@@ -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" "$@"
|
||||
Executable
+36
@@ -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."
|
||||
@@ -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>
|
||||
Executable
+28
@@ -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
|
||||
@@ -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()
|
||||
Executable
+10
@@ -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" "$@"
|
||||
Executable
+10
@@ -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" "$@"
|
||||
Executable
+20
@@ -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" "$@"
|
||||
@@ -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
@@ -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;\"
|
||||
"
|
||||
Executable
+10
@@ -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"
|
||||
Executable
+10
@@ -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"
|
||||
Executable
+10
@@ -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"
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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)
|
||||
Executable
+40
@@ -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
|
||||
@@ -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
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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
@@ -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()
|
||||
Executable
+10
@@ -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" "$@"
|
||||
Executable
+10
@@ -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" "$@"
|
||||
Executable
+16
@@ -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" "$@"
|
||||
@@ -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)
|
||||
@@ -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
|
||||
@@ -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)
|
||||
Reference in New Issue
Block a user