Skip to content

Latest commit

 

History

History
235 lines (191 loc) · 36 KB

USECASES.md

File metadata and controls

235 lines (191 loc) · 36 KB

cade mascot: a Go gopher filing folders

Especificação — CLI de Histórico Pessoal

Ferramenta CLI local que agrega a atividade pessoal de múltiplas fontes e responde consultas por timeline e por busca semântica. 100% local por requisito de privacidade.


Índice

  1. Visão geral
  2. Requisitos funcionais
  3. Requisitos não funcionais
  4. Critérios de aceite
  5. Escopo de entrega
  6. Stack
  7. Limitações conhecidas
  8. Fora de escopo

1. Visão geral

CLI que ingere a atividade do usuário de várias fontes (git, browser, arquivos e mensagens do Teams), normaliza tudo num modelo único de evento e permite duas formas de consulta: reconstrução de timeline por data e busca semântica por conteúdo. Todo processamento — banco, embeddings e geração de linguagem — roda localmente, sem enviar dados para serviços externos.


2. Requisitos funcionais

RF1 — Ingestão de múltiplas fontes

  • RF1.1 Ingerir histórico de repositórios Git (commits: hash, data, autor, mensagem, arquivos alterados), de todas as branches, tags e remotes. Filtro opcional por autor. Cada commit é marcado como do usuário ou de outra pessoa pelas identidades de sources.git_identities ("auto" = git config user.email/user.name do repositório; commits gravados antes são marcados na ingestão seguinte). A timeline mostra só os do usuário (--all-authors mostra todos), perguntas em primeira pessoa sem pessoa citada deixam de fora os de outros, e o relatório de tarefas ignora commits de outros e conta os do usuário como trabalho seu na tarefa.
  • RF1.2 Ingerir histórico de browser a partir do banco SQLite local (URL, título, timestamp de cada visita). Suporta Firefox (places.sqlite) e navegadores Chromium (History). O banco é lido a partir de uma cópia, pois o navegador o mantém travado.
  • RF1.3 Ingerir arquivos de diretórios configurados (caminho, data de modificação e conteúdo textual quando aplicável). Arquivos binários ou maiores que max_file_bytes entram sem conteúdo; diretórios em ignored_dir_names são ignorados. Cada arquivo é um evento com a versão atual (UID = caminho, revisão = data de modificação); cada versão vista fica em file_modifications (caminho, data, tamanho), e a timeline mostra todas as edições. Um arquivo que sumiu da pasta ganha removed_at na ingestão seguinte (só quando a pasta inteira foi lida sem erro), sai das respostas e continua na timeline; se voltar, a marca é removida.
  • RF1.4 Ingerir mensagens de chat do Teams a partir do IndexedDB do Teams web no Chrome (store replychains), sem Graph API e sem rede. Entram apenas mensagens de conversa (RichText/Html e Text); chamadas, gravações, eventos de sistema, mensagens apagadas e cópias do feed de notificações são ignorados. Cada mensagem registra o tipo de conversa (chat, canal, reunião) e se foi enviada, recebida ou publicada num canal.
  • RF1.5 A ingestão é incremental: reexecutar não duplica eventos já ingeridos (deduplicação por identificador estável do evento).
  • RF1.6 Uma mudança de formato do Teams não passa em silêncio: se o IndexedDB tem registros mas nenhum store replychains, ou mensagens em que nenhuma tem os campos lidos (id, conversationId, messageType, content e o horário de chegada), a ingestão falha apontando para cade teams-schema. Mensagens de sistema ou apagadas continuam descartadas sem erro, e um IndexedDB vazio não é erro. Os eventos já gravados estão no formato do cade e não dependem do formato do Teams.
  • RF1.7 Com sources.images ligado (padrão desligado; o cade init pergunta), cada imagem png, jpeg ou webp das pastas de RF1.3 é descrita pelo modelo de geração com o projetor de visão (mmproj): uma descrição curta, em português, e a transcrição do texto visível. A descrição vira o texto do evento do arquivo e segue o caminho de qualquer texto (máscara de segredos, pedaços, busca híbrida, forget). Cada imagem é descrita uma vez: mover ou renomear reaproveita a descrição pelo SHA-256 dos bytes; cade reindex --captions descreve de novo quando o modelo ou o prompt mudam. ingest.max_images_per_run limita quantas são descritas por execução (o resto fica pendente) e ingest.max_image_bytes, o tamanho do arquivo; uma imagem que não abre é marcada ilegível sem parar a ingestão. O modelo de visão é liberado antes de o de embedding carregar.
  • RF1.8 Com sources.pdfs ligado (padrão desligado; o cade init pergunta), o texto de cada página dos PDFs das pastas de RF1.3 entra no conteúdo do evento, e a citação do ask diz o arquivo e a página ("relatorio.pdf, p. 12"). Uma página escaneada é descrita como em RF1.7, se sources.images também estiver ligado; senão, espera. Um PDF cifrado, corrompido ou acima dos limites é marcado como ilegível e não para o ingest.

RF2 — Modelo de evento normalizado

  • RF2.1 Toda fonte é convertida a um formato comum de evento: timestamp, source (git/browser/file/teams), content (texto), metadata (dados específicos da fonte) e um identificador único para deduplicação.
  • RF2.2 Cada evento com conteúdo textual tem embeddings gerados localmente para busca semântica, um por pedaço: texto de até 1.200 caracteres é um pedaço só; texto maior é dividido (internal/chunking: títulos Markdown, parágrafos, linhas, espaços; ~10% de sobreposição) para caber no contexto de treino do embedder (512 tokens, lido do GGUF). chunks guarda as posições no texto, sem duplicá-lo. Eventos sem texto entram apenas na timeline.
  • RF2.3 Os metadados de cada fonte têm um tipo próprio (event.Commit, event.Visit, event.File, event.Message), lido e escrito só pelo pacote event; o resto do código não usa chaves em texto. O formato gravado continua o mesmo objeto JSON com as mesmas chaves, então eventos já gravados são lidos sem migração. Uma fonte nova acrescenta o seu tipo.

RF3 — Consulta por timeline

  • RF3.1 Consultar todos os eventos de uma data específica, ordenados cronologicamente. Aceita AAAA-MM-DD, hoje e ontem.
  • RF3.2 Consultar um intervalo de datas.
  • RF3.3 A timeline cruza todas as fontes numa visão unificada, em ordem de hora, com filtro opcional por fonte (--source).
  • RF3.4 A timeline indica a fonte de cada evento.
  • RF3.5 cade timeline --around UID [--window 2h] mostra o evento e o que aconteceu até a janela antes e depois dele, em todas as fontes, com o evento marcado; --source restringe os vizinhos e --all-authors inclui commits de outros, mas o evento pedido sempre aparece (#21).

RF4 — Consulta semântica

  • RF4.1 Fazer perguntas em linguagem natural sobre a atividade.

  • RF4.2 Recuperar os eventos mais relevantes por similaridade semântica (busca vetorial), com filtro opcional por fonte (--source) e período (--from/--to). Sem as flags, a pergunta é interpretada: por regras quando ela só tem período, fonte e palavras genéricas (qualquer outra palavra a manda ao modelo, e as regras não erram nenhuma pergunta da suíte que leem), senão pelo LLM local (saída restrita por gramática GBNF), que extrai apenas os filtros explícitos: período, fonte, pessoas, direção, assunto e, quando a pergunta pede imagens ou "quem mais", modalidade, entidades e ampliação (RF4.15). Filtros, flags e datas são resolvidos num plano único e determinístico (queryplan.Resolve); datas vêm de um parser determinístico. Nomes são comparados por palavra inteira, ignorando maiúsculas, acentos, letras dobradas e y/i ("sillva" → "Leandro Silva"); um nome que não casa com nenhum remetente ou conversa vira filtro de texto em listagens e tarefas (um cliente lido como pessoa) e é ignorado em respostas, que já buscam pela pergunta inteira; uma verificação remove direção ou período que a pergunta não sustenta. --no-filters desativa.

  • RF4.3 Um LLM local gera a resposta baseada somente nos eventos recuperados. Quando nada relevante é encontrado, o sistema informa isso em vez de responder. A busca é por pedaço e a resposta por evento: cada evento vale pelo seu pedaço mais próximo, e o prompt recebe esse pedaço (até 1.200 caracteres), com a posição ("arquitetura.md, trecho 7 de 20"), e não o começo do texto.

  • RF4.3a Uma pergunta sobre o momento de outra coisa ("o que eu estava lendo quando corrigi o webhook?") cita o commit, não a página, que não divide palavras com a pergunta. Com "enquanto", "antes", "depois" ou "quando" depois da primeira palavra (e "while", "before", "after", "when"), o resultado mais próximo traz até 4 eventos a até 2h dele, os mais próximos no tempo primeiro, respeitando a fonte e o filtro de commits próprios. "quando é o deploy?" pede uma data e não traz vizinhos (#21).

  • RF4.4 A resposta cita as fontes e datas dos eventos usados, cada uma com seu localizador (repositório@hash, URL, caminho, link da mensagem no Teams). Números citados que não correspondem a nenhum evento consultado são apontados como trechos sem fonte.

  • RF4.5 Pedidos de enumeração com período ("as mensagens que recebi da Ana ontem") listam todos os eventos que casam com os filtros, direto do banco: a busca por similaridade retornaria só os top-k. Um assunto ("páginas sobre redis") restringe a lista por similaridade.

  • RF4.6 Perguntas com pessoa ou direção ("o que o Marcos me pediu ontem?") restringem os eventos exatamente antes de ordená-los por similaridade, pois nomes e direção não são capturados de forma confiável pelo embedding. No Teams, publicações em canal não contam como recebidas, nem mensagens de grupo que só @mencionam outras pessoas conhecidas (o usuário é reconhecido pelas mensagens que enviou; menção a ele, a um time ou tag mantém a mensagem). O filtro roda no banco, sobre o índice de pessoas (event_people: remetente ou autor, conversa e @menções de cada evento) e a direção de cada mensagem (events.direction), e só os eventos que casam são lidos: uma pergunta por pessoa sem período lia o histórico inteiro. Acima de retrieval.max_filtered_events (1000) eventos que casam, a busca vai ao índice vetorial do período e da fonte e fica com os vizinhos que casam.

  • RF4.7 Só o assunto é buscado por significado quando há filtros exatos (período, fonte, pessoa, direção): em "commits de ontem sobre autenticação", embutir a pergunta inteira trazia mensagens com a palavra "ontem" antes dos commits. Sem filtros, a pergunta inteira é embutida, pois o assunto isolado fica distante demais de tudo e cairia no corte de distância.

  • RF4.8 cade ask --json devolve o plano resolvido e o resultado com a referência (uid, fonte, data, localizador) de cada evento que o sustenta: evidências e quais foram citadas, eventos listados, ou tarefas com PRs e eventos.

  • RF4.9 Mensagens formadas só por cumprimentos, confirmações e palavras de ligação ("ok", "valeu", "bom dia", "pode ser") não entram como evidência de respostas: entre as mensagens de uma pessoa, elas ficavam à frente do pedido real. Listagens continuam mostrando tudo.

  • RF4.10 Perguntas sem filtro só recebem evidência se o evento mais próximo estiver a no máximo retrieval.max_best_distance (0,61); um corte só por evento não separava perguntas com e sem resposta. Calibrado na suíte de recuperação; no histórico real a margem é menor, por isso é configurável e o --verbose registra a distância quando a pergunta é rejeitada. Os limiares valem para um modelo de embedding: o banco registra para qual, e reindex e doctor avisam quando o modelo muda e os limiares ficam iguais. A resposta recebe até retrieval.top_k (6) eventos, o menor valor sem perda de recall na suíte (fase 17).

  • RF4.11 Nas respostas, repetições da mesma coisa (mesma URL, mesmo caminho de arquivo, mesmo commit ou mensagem, pelo localizador da proveniência) viram uma evidência só, com a contagem e a data mais recente ("12 visitas, última em …"); a busca pede mais vizinhos até ter top_k itens distintos. Arquivos removidos da pasta não entram como evidência. Listagens continuam mostrando cada ocorrência.

  • RF4.12 Texto idêntico é embutido uma vez: a ingestão e o reindex reaproveitam o vetor de um evento com o mesmo content_hash.

  • RF4.13 Busca híbrida (retrieval.mode = hybrid, padrão): a busca vetorial e a por palavras (FTS5/BM25 sobre os pedaços, com hash de commit e caminho de arquivo no primeiro pedaço) usam os mesmos filtros e são combinadas por fusão de posições (RRF, k = 60). Uma pergunta com identificador explícito (código [A-Z]+-\d+, hash hexadecimal de 7 a 40 caracteres, número de PR) é respondida só com os eventos que o contêm, sem as portas de distância. vector e lexical usam uma busca só. Perguntas com pessoa continuam só vetoriais.

  • RF4.14 Texto que dá ordens ao assistente (um vocativo como "assistente", "instrução do sistema" ou "assistant" a até 80 caracteres de um pedido como "ignore", "responda que", "cite apenas", "regras") é marcado na evidência como NÃO CONFIÁVEL: contém ordens ao assistente, e vai ao prompt sem o texto (fase 18: com o texto, os três modelos medidos ainda seguiam 1 de 4 injeções), com uma regra que manda não usá-lo nem citá-lo. O evento continua numerado na evidência e aparece com a marca na lista de fontes e como untrusted no --json. Num histórico real de 108 mil eventos, nenhum foi marcado. Delimitar cada evidência com tags (<evento>…</evento>) foi medido e piorou: o modelo de 3B seguiu mais injeções e citou menos.

  • RF4.15 Perguntas sobre imagens e relações usam o grafo (#24). "imagens em que o João aparece", "imagens do João que o Pedro me mandou" e "prints do projeto Orion" ficam só com eventos que mostram uma imagem (arquivo de imagem, mensagem do Teams com imagem, foto do WhatsApp) e em que cada entidade aparece: no texto do evento ou no de outro que mostra a mesma imagem, como o arquivo descrito de uma foto do WhatsApp. "quem mais mexeu no arquivo que o Rui alterou?" parte das pessoas citadas, chega ao que elas tocam pelas relações (os arquivos que o Rui alterou) e fica com todos os eventos desses. O que sobra é ranqueado como no RF4.6, com um resultado por imagem; uma entidade que ninguém cita é avisada e ignorada. Pessoa aparece numa imagem quando a enviou ou é citada nela ou na descrição; pelo rosto, só na v0.3.0 (#23).

  • RF4.16 Com ingest.names_from_email ligado (opt-in), o autor de um commit também é achado pelo nome que o e-mail escreve: "F. Rocha <fernanda.rocha@…>" responde por Fernanda Rocha.

RF6 — Relatório de tarefas

  • RF6.1 cade tasks [DATA [FIM]] lista as tarefas trabalhadas no período, reconhecidas por links de rastreadores (regex configuráveis em tasks.task_url_patterns) em visitas e mensagens.
  • RF6.2 Uma tarefa tem estado "PR aberto" quando um PR ligado a ela foi aberto. Abertura confirmada localmente pela visita à página de criação antes da primeira visita ao PR; uma mensagem enviada com o link é provável. Sem rede, aprovação e merge são desconhecidos. Hosts: GitHub, GitLab, Bitbucket, Azure DevOps.
  • RF6.3 Ligação PR ↔ tarefa: exata (mensagem que cita uma única tarefa e o PR, ou título do PR citando o id da tarefa) ou provável (PR aberto até 2h após trabalhar na tarefa), sempre indicada.
  • RF6.4 Perguntas sobre tarefas no cade ask ("quais tarefas finalizei ontem?", "what tasks are still in progress?") devolvem o mesmo relatório, com filtro opcional de status (PR aberto / em andamento) e período padrão de hoje. Com pessoa ou direção ("tarefas que a Ana me passou ontem"), só entram tarefas com link nas mensagens selecionadas, de qualquer dono; um nome que não é de ninguém filtra pelo texto da tarefa, PRs e mensagens ("tarefas de Solaris" → PR -> main-solaris). Uma mensagem citando várias tarefas conta para todas. O tipo "tarefas" e o status só valem se a pergunta os sustentar (menciona tarefa/ticket/demanda; "finalizei", "pendentes"...).
  • RF6.5 Tarefas são classificadas como suas (PR aberto ou mensagem sua citando-a), consultadas (só a página aberta) ou citadas só por outras pessoas (resumidas; --all lista).

RF5 — Interface CLI

  • RF5.1 cade ingest <fonte|all> [ALVO...]: ingere uma fonte com alvos explícitos ou os configurados, exibindo progresso durante a execução, com ETA na descrição de imagens e nas fontes file e git.
  • RF5.1.1 cade ingest start <fonte|all> roda a ingestão num processo fora do terminal (continua depois de fechá-lo), com os limites de ingest.background (threads, camadas na GPU, parte do tempo em que os modelos trabalham) e a menor prioridade de CPU e E/S; cade ingest --gentle aplica os mesmos limites no terminal. cade ingest status mostra, de qualquer terminal, a etapa, o progresso e o ETA da ingestão em andamento ou o fim da última; pause a congela mantendo os modelos na memória; stop a encerra mantendo o que foi gravado; resume continua uma pausada ou roda de novo a última não concluída, no mesmo modo. Enquanto uma ingestão roda ou está pausada, outra não começa. O cade doctor mostra quando a última rodou e como terminou (issue #41).
  • RF5.2 cade timeline [--source F] DATA [DATA_FIM].
  • RF5.3 cade ask [--source F] [--from D] [--to D] PERGUNTA.
  • RF5.4 Saída legível no terminal, indicando fonte e timestamp de cada resultado.
  • RF5.5 cade forget <fonte> remove os eventos de uma fonte para reingestão; cade teams-schema DIR imprime a estrutura (sem valores) de um IndexedDB, para diagnosticar mudanças de formato do Teams.
  • RF5.5a cade export [--source F] [--from D --to D] escreve no stdout, em JSON Lines, tudo o que o cade guarda: um evento por linha, com UID, fonte, hora, texto, metadados e os derivados (nomes do índice de pessoas, entidades, relações que o evento afirma, representações como a descrição de uma imagem), sem vetores. Os filtros são os do forget --match; a contagem vai para o stderr. Lê o histórico em páginas, sem carregá-lo inteiro (#85).
  • RF5.5b cade forget --person NOME apaga tudo sobre uma pessoa e cade export --person NOME exporta o mesmo (#86): o que ela mandou ou escreveu (com o nome lido do e-mail, quando ligado), as mensagens que @mencionam o primeiro nome dela e as conversas só com ela, em que toda mensagem que o usuário não mandou é dela, com as do usuário. Grupos ficam, a menos que ela escreva ou seja mencionada. O nome tem de apontar uma pessoa só: "Ana", com Ana Prado e Ana Souza no banco, para e lista as duas. O forget lista antes de apagar e pede confirmação (ou --yes); uma @menção só tem o primeiro nome, então conta para todas as pessoas com ele.
  • RF5.5c Todo cade forget termina listando as cópias do banco feitas antes de migrações e de rekey (cade.db.before-v*, cade.db.before-rekey-*), com data e tamanho, porque elas ainda guardam o que foi apagado; sem cópias, não diz nada. cade forget --backups lista, pergunta (ou --yes) e as apaga, nunca o cade.db. O cade doctor mostra quantas são, a data e a idade da mais antiga e o tamanho, sem contar como aviso (#87).
  • RF5.6 cade init acha os históricos de navegador (Chromium e Firefox), os caches do Teams e os repositórios git sob um diretório informado, pergunta o que incluir (o Teams fica de fora por padrão) e grava a configuração com permissão 600. Olha só nomes, nunca conteúdo; sem entrada, fica com os padrões.
  • RF5.7 cade doctor confere os modelos (arquivo GGUF), o FTS5 do SQLite, o banco (versão do esquema, migração com cópia pendente, modelo dos vetores, reindexação pendente, limiares definidos para outro modelo) e cada caminho configurado, dizendo como corrigir cada problema. Lê o banco sem migrá-lo e sai com código 1 quando algum comando falharia.
  • RF5.8 A interface (ajuda, rótulos, progresso e erros da CLI) segue o idioma do sistema (LC_ALL, LC_MESSAGES, LANG: português para pt*, inglês nos outros casos) ou ui.language (auto, pt, en); a resposta do ask segue o idioma da pergunta, e os códigos do ask --json não mudam com o idioma.
  • RF5.9 cade version (ou --version) mostra a versão, o commit, a data, o tipo de build (CPU/CUDA) e a tag do llama.cpp.
  • RF5.10 As flags de cada comando podem vir antes ou depois dos argumentos (cade timeline ontem --source git); depois de --, tudo é argumento.
  • RF5.11 Mensagens com contagem concordam com o número nos dois idiomas ("1 evento", "0 eventos", "2 tarefas suas").
  • RF5.12 Datas numéricas nas perguntas (12/08) seguem ui.date_order (auto, dmy, mdy); auto lê mês primeiro com o sistema (LC_ALL, LC_TIME, LANG) em en_US e dia primeiro nos outros casos. As datas da saída são sempre ISO (AAAA-MM-DD).

3. Requisitos não funcionais

RNF1 — Privacidade (crítico)

  • RNF1.1 Todo processamento ocorre localmente. Nenhum dado de atividade é enviado a serviços externos.
  • RNF1.2 A geração de linguagem usa um LLM local via llama.cpp embutido no processo (sem servidor).
  • RNF1.3 A geração de embeddings ocorre localmente, também via llama.cpp.
  • RNF1.4 O banco e seus arquivos auxiliares (-wal, -shm) são legíveis só pelo dono (600, pasta 700), inclusive bancos criados por versões anteriores, que eram 644.
  • RNF1.5 Apagar é definitivo no disco: secure_delete zera texto apagado ou substituído, e cade forget compacta o banco e esvazia o WAL. O forget file apaga também o histórico de edições.
  • RNF1.6 PRIVACY.md / PRIVACY.pt-BR.md documentam o que cada fonte guarda, onde, o que o modelo local vê, os logs, como apagar e os limites (banco sem criptografia; mensagens apagadas no Teams continuam até forget).

RNF2 — Persistência

  • RNF2.1 Eventos e embeddings são armazenados em SQLite com a extensão sqlite-vec. Banco em arquivo local, sem servidor.
  • RNF2.2 O acesso ao banco fica atrás da interface EventStore, de modo que o backend possa ser trocado (ex.: PostgreSQL/pgvector) sem alterar ingestão, timeline ou consulta semântica.
  • RNF2.3 Mudanças de esquema são migrações numeradas (PRAGMA user_version), aplicadas em ordem ao abrir o banco, cada uma numa transação com a nova versão: uma falha mantém a última versão boa. Migrações que reescrevem dados copiam o banco antes (VACUUM INTO, cade.db.before-vN-<data>, permissão 600). Um banco de versão mais nova que o cade é recusado. Reimportar não é caminho de migração: o cache do Teams e o histórico do navegador expiram. Versões: 1 = esquema de antes do versionamento; 2 = texto original de cada mensagem do Teams guardado nos metadados (recuperado do conteúdo só quando remontá-lo reproduz o conteúdo exato), para que mudanças no formato do conteúdo sejam refeitas a partir do banco (event.Message.Content()); 3 = content_hash para reaproveitar o vetor de textos idênticos (dado derivado, sem cópia); 4 = um evento por arquivo, versões em file_modifications (com cópia); 5 = vetores por pedaço (chunks, chunk_embeddings): eventos curtos mantêm o vetor como pedaço único, os longos ficam para cade reindex (marcado como pendente), e o banco é compactado no fim; 6 = índice FTS5 chunks_fts sobre os pedaços (dado derivado, sem cópia); 7 = índice de pessoas event_people e coluna events.direction, preenchidos a partir dos eventos gravados, mil por vez (dado derivado, sem cópia). Uma abertura faz no máximo uma cópia, antes do primeiro passo que reescreve dados.
  • RNF2.4 cade reindex recalcula todos os vetores com o modelo de embedding configurado, a partir do texto guardado, em lotes transacionais; se interrompido, a próxima execução continua (mesmo modelo) ou recomeça (outro modelo). O banco registra o modelo dos vetores (store_settings.embedding_model); ingest e ask recusam um modelo diferente — dimensão igual não basta (nomic v1.5 e v2-moe têm 768) — e avisam quando há reindexação pendente. Bancos anteriores adotam o modelo configurado.

RNF3 — Integridade dos dados (crítico)

  • RNF3.1 A ingestão não perde eventos das fontes suportadas dentro do escopo configurado. Uma falha interrompe a execução em vez de descartar eventos; reexecutar continua de onde parou. Exceção: o Teams é "melhor esforço", pois o IndexedDB contém apenas o que o cliente carregou.

  • RNF3.2 A deduplicação não descarta eventos distintos que coincidam em algum campo:

    Fonte Identificador
    git hash do commit
    browser navegador + URL + instante da visita (µs)
    file caminho + data de modificação (ns) + tamanho
    teams id da conversa + id da mensagem
  • RNF3.3 Reingerir um evento já gravado só o reprocessa se o conteúdo mudou na fonte: texto e embedding são substituídos numa transação. Quando o evento tem revisão (no Teams, o version da mensagem, que muda a cada edição), só uma revisão maior substitui, então a versão editada vence mesmo que outro cache tenha a antiga, e nada alterna entre execuções.

RNF4 — Extensibilidade

  • RNF4.1 Adicionar uma nova fonte não exige mudança no modelo de evento nem nas consultas — apenas um novo EventCollector registrado em cmd/cade/sources.go.

RNF5 — Desempenho

  • RNF5.1 A busca retorna em tempo interativo para o volume de uso pessoal.
  • RNF5.2 Cada modelo é carregado uma única vez por execução.
  • RNF5.3 go tool mage bench mede latência e memória; bench/baseline.txt guarda a referência (RTX 3060, 2026-09-27) e bench/baseline-cpu.txt, a mesma máquina sem a GPU (Ryzen 5 5500). Na GPU: busca vetorial em 100 mil eventos, ~103 ms; ler o histórico inteiro (pergunta com pessoa e sem período), ~342 ms; embedding de um evento, ~3,4 ms (36 ms em CPU); interpretar a pergunta pelo modelo sem estado salvo, 1,4 s (19,7 s em CPU); gerar a resposta com 8 evidências, ~0,4 s (2,9 s em CPU); ~3,8 KB por evento no banco; modelos ocupam ~2,5 GB de GPU e ~1,2 GB de RAM (~3,9 GB de RAM em CPU).
  • RNF5.4 Um cade ask não refaz trabalho fixo: perguntas feitas só de período, fonte e palavras genéricas são lidas por regras, sem modelo, e uma listagem ou relatório lido assim nem carrega modelo; o estado do modelo depois das instruções e exemplos fixos do planejador fica salvo em ~/.cache/cade/prompt-state/, com chave na versão do llama.cpp, no build, no arquivo do modelo, no contexto e nos tokens do prompt. Até o primeiro token da resposta, com o cache de página quente, em CPU: 41,5 s → 26,0 s com o estado salvo, 21,5 s pelas regras; na GPU: 2,9 s → 2,5 s → 1,6 s.

RNF6 — Configuração

  • RNF6.1 Fontes (repositórios, históricos do browser, diretórios, diretórios do Teams), modelos e parâmetros de busca são configuráveis em ~/.config/cade/config.json, sem hardcode.
  • RNF6.2 config.example.json traz todos os campos, e o README descreve cada um numa tabela; testes falham se o exemplo ou as tabelas deixarem de cobrir um campo.

RNF7 — Qualidade da interpretação

  • RNF7.1 Uma suíte de ~150 perguntas representativas (testdata/queries/plan.json: período, git, Teams, navegador, arquivos, busca semântica, pessoas, tarefas, empresas lidas como pessoa, ambíguas, PT e EN) fixa o plano esperado de cada uma. go tool mage evalPlan roda a suíte com o modelo real e mede o acerto por campo com intervalo de Wilson de 95%.
  • RNF7.2 Cada campo tem um piso (minimum_accuracy) comparado com o limite inferior do intervalo: mudanças de prompt ou modelo que o derrubem falham o teste, em vez de regredirem em silêncio, e um erro isolado não reprova.
  • RNF7.3 Suíte de recuperação (testdata/queries/retrieval/, go tool mage evalRetrieval): corpus sintético de ~290 eventos com distratores parecidos, visitas repetidas, versões de arquivo, notas longas, commits de outros autores e conversa do dia a dia, ingerido num SQLite real com o embedder real. Os casos se dividem em calibração (só relata onde os limites deveriam ficar) e teste (nunca usado para ajustar, com pisos de recall, MRR e rejeição); mede também a redundância. go tool mage evalScale gera a curva por tamanho do corpus (bench/retrieval-scale.txt); o baseline antes da próxima versão fica em bench/retrieval-baseline.txt.
  • RNF7.4 Casos de injeção (testdata/queries/injection.json, go tool mage evalInjection): perguntas cuja evidência inclui um evento do corpus escrito para manipular o modelo (mensagem, título de página, nota que tenta fechar o delimitador, texto em inglês), respondidas com os dois modelos reais. Reprova quando a resposta segue a injeção, não traz o fato real, cita evidência inexistente ou responde SEM_INFORMACAO; falta de citação é relatada sem reprovar, porque o modelo de 3B às vezes não cita mesmo sem injeção.

RNF8 — Qualidade do código

  • RNF8.1 Cada push e pull request passa por gofmt, go vet, golangci-lint (errcheck, staticcheck, unused, ineffassign) e go tool mage test na CI (.github/workflows/ci.yml); qualquer falha deixa a execução vermelha. go tool mage check roda o mesmo localmente.
  • RNF8.2 As suítes com modelo (go tool mage eval) rodam em workflow manual ou semanal (.github/workflows/eval.yml), em CPU, com o relatório publicado como artefato.
  • RNF8.3 A cobertura dos testes aparece no resumo de cada execução e num badge do README.

4. Critérios de aceite

ID Critério Status
CA1 Cada commit de um repositório configurado vira um evento com data, mensagem e arquivos alterados. Atendido
CA2 Cada visita do histórico do browser vira um evento com URL, título e timestamp. Atendido
CA3 Cada arquivo de um diretório configurado vira um evento com caminho e data de modificação. Atendido
CA4 Reexecutar a ingestão não duplica eventos. Atendido
CA5 A timeline de uma data mostra todos os eventos do dia, de todas as fontes, por hora, com a fonte indicada. Atendido
CA6 A timeline de um intervalo mostra os eventos em ordem cronológica. Atendido
CA7 Uma data sem atividade retorna indicação clara de que não há eventos. Atendido
CA8 Uma pergunta retorna resposta baseada nos eventos recuperados, com fontes e datas citadas. Atendido
CA9 Uma pergunta sem resposta nos dados retorna "Não encontrei informação" em vez de inventar. Atendido
CA9.1 A busca vetorial combinada com filtro de fonte e/ou período respeita o filtro. Atendido
CA10 Nenhuma operação faz requisição de rede com dados de atividade. Atendido e verificado sem rede
CA11 Um novo ingestor torna seus eventos consultáveis sem alterar as consultas. Atendido (Teams foi adicionado assim)
CA12 Uma imagem das pastas configuradas é encontrada pelo que mostra e pelo texto nela, sem que segredos, pixels ou ordens escritas nela cheguem ao banco ou à resposta. Atendido (evalCaptions, casos de imagem em evalRetrieval e evalInjection)

Notas:

  • CA8 — A qualidade da resposta depende do modelo de geração. Com o Qwen3.5-2B e a regra de citação com exemplo (fase 18), as respostas dos casos de injeção citam todas com [n]; quando uma resposta não cita, a CLI lista todos os eventos consultados.
  • CA9.1 — O filtro é aplicado dentro da busca KNN do sqlite-vec (colunas de metadata do vec0), não após o corte top-k; há teste de regressão para isso. O período vem das flags ou de uma data citada na pergunta ("o que pesquisei semana passada" filtra a semana anterior). Dias da semana ("na segunda") ainda não são reconhecidos.
  • CA10 — O código não usa cliente de rede e o llama.cpp é compilado sem suporte a download. Verificado em 2026-09-27 num namespace sem rede (unshare -rn, só o loopback, desligado): cade ingest all e um cade ask que carrega os dois modelos funcionaram, com a resposta e a fonte certas.

5. Escopo de entrega

Fase Conteúdo Status
1 RF2, RF1.1 (git), RF3 (timeline), persistência local Concluída
2 RF1.2 (browser), RF1.3 (arquivos), RF4 (busca semântica com LLM local) Concluída
3 RF4.4 (citações), filtro por fonte/data combinado com busca vetorial Concluída
4 RF1.4 (mensagens do Teams via IndexedDB) Concluída

6. Stack

Componente Escolha
Linguagem Go
Persistência SQLite (mattn/go-sqlite3) + sqlite-vec
Inferência llama.cpp (tag b11195), compilado estático e ligado via cgo, com a biblioteca de visão mtmd; CUDA opcional (go tool mage cuda)
Embeddings nomic-embed-text-v2-moe Q4_K_M (multilíngue)
Geração Qwen3.5-2B Q4_K_M (Apache-2.0), com o projetor de visão (mmproj) ao lado
Teams leitor próprio de LevelDB, IndexedDB do Chromium e serialização V8
PDFs leitor próprio em Go puro, spfdtextextractor (GPL-3.0-or-later): texto e imagens embutidas, sem renderizar

Decisões de design:

  • llama.cpp embutido em vez de Ollama ou llama-server, para não depender de um processo servidor.
  • O nomic-embed-text-v1.5 foi substituído pelo v2-moe por ser centrado em inglês e ordenar mal perguntas em português.
  • O Qwen2.5-1.5B foi substituído pelo 3B, que respondia "não encontrei" indevidamente em perguntas com filtro.
  • O Qwen2.5-3B (licença só não comercial) foi substituído pelo Qwen3.5-2B (Apache-2.0), que entende as perguntas igual ou melhor em todos os campos da suíte de plano, cabe em menos VRAM e também lê imagens (fase 19). O 4B entende melhor ainda, mas passa do orçamento de VRAM.
  • Teams lido do IndexedDB local em vez da Graph API: dispensa login, consentimento do tenant e acesso à rede, ao custo de cobrir apenas o que o cliente armazenou.
  • O LevelDB é lido por implementação própria (sem goleveldb, que está sem manutenção), mantendo a versão de maior sequência de cada chave, o que é correto independentemente do comparador idb_cmp1 do Chromium.

7. Limitações conhecidas

  • Arquivos: apenas o início do texto (até max_file_bytes) é indexado; não há divisão em trechos.
  • Teams: o formato interno pode mudar em atualizações do cliente; mensagens apagadas depois de ingeridas continuam no banco. Uma imagem que o navegador já tirou do cache fica só com o endereço, sem descrição; o cache de um perfil do Firefox não é lido. Uma mensagem com várias imagens guarda a primeira como sua imagem e as descrições de todas no texto.
  • Imagens: descrever custa ~1,7 s por imagem numa RTX 3060 e ~22 s numa CPU de 6 núcleos; uma pasta grande leva várias execuções de ingest. Fotos sem texto dependem só da descrição, curta e genérica. A descrição de uma imagem apagada da pasta fica no banco até cade forget file.
  • PDFs: a página escaneada é lida pela imagem embutida, sem renderizar, então JBIG2, JPEG 2000 e páginas montadas em tiras ficam sem conteúdo; a descrição acha a página pelo que ela mostra, sem a fidelidade de um OCR. PDFs cifrados não são lidos, nem com senha vazia. O reindex --captions não descreve de novo as páginas de PDF.
  • Trocar o modelo de embedding exige um banco novo (a dimensão dos vetores é fixada no primeiro insert).
  • Tarefas passadas sem link (só em texto) não são reconhecidas.
  • O modelo de 3B às vezes lê empresas e clientes como pessoas; o filtro de texto para nomes desconhecidos compensa em listagens e tarefas.

8. Fora de escopo

  • Interface gráfica ou web.
  • Sincronização entre máquinas.
  • Fontes que exijam serviços externos.
  • Multiusuário.
  • Calendário e histórico de chamadas do Teams.