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.
- Visão geral
- Requisitos funcionais
- Requisitos não funcionais
- Critérios de aceite
- Escopo de entrega
- Stack
- Limitações conhecidas
- Fora de escopo
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.
- 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.namedo repositório; commits gravados antes são marcados na ingestão seguinte). Atimelinemostra só os do usuário (--all-authorsmostra 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_bytesentram sem conteúdo; diretórios emignored_dir_namessão ignorados. Cada arquivo é um evento com a versão atual (UID = caminho, revisão = data de modificação); cada versão vista fica emfile_modifications(caminho, data, tamanho), e a timeline mostra todas as edições. Um arquivo que sumiu da pasta ganharemoved_atna 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/HtmleText); 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,contente o horário de chegada), a ingestão falha apontando paracade 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.imagesligado (padrão desligado; ocade initpergunta), 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 --captionsdescreve de novo quando o modelo ou o prompt mudam.ingest.max_images_per_runlimita quantas são descritas por execução (o resto fica pendente) eingest.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.pdfsligado (padrão desligado; ocade initpergunta), o texto de cada página dos PDFs das pastas de RF1.3 entra no conteúdo do evento, e a citação doaskdiz o arquivo e a página ("relatorio.pdf, p. 12"). Uma página escaneada é descrita como em RF1.7, sesources.imagestambém estiver ligado; senão, espera. Um PDF cifrado, corrompido ou acima dos limites é marcado como ilegível e não para oingest.
- 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).chunksguarda 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 pacoteevent; 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.1 Consultar todos os eventos de uma data específica, ordenados cronologicamente. Aceita
AAAA-MM-DD,hojeeontem. - 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;--sourcerestringe os vizinhos e--all-authorsinclui commits de outros, mas o evento pedido sempre aparece (#21).
-
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-filtersdesativa. -
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 deretrieval.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 --jsondevolve 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--verboseregistra a distância quando a pergunta é rejeitada. Os limiares valem para um modelo de embedding: o banco registra para qual, ereindexedoctoravisam 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_kitens 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
reindexreaproveitam o vetor de um evento com o mesmocontent_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.vectorelexicalusam 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 comountrustedno--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_emailligado (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.1
cade tasks [DATA [FIM]]lista as tarefas trabalhadas no período, reconhecidas por links de rastreadores (regex configuráveis emtasks.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;
--alllista).
- 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 fontesfileegit. - 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 deingest.background(threads, camadas na GPU, parte do tempo em que os modelos trabalham) e a menor prioridade de CPU e E/S;cade ingest --gentleaplica os mesmos limites no terminal.cade ingest statusmostra, de qualquer terminal, a etapa, o progresso e o ETA da ingestão em andamento ou o fim da última;pausea congela mantendo os modelos na memória;stopa encerra mantendo o que foi gravado;resumecontinua 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. Ocade doctormostra 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 DIRimprime 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 doforget --match; a contagem vai para o stderr. Lê o histórico em páginas, sem carregá-lo inteiro (#85). - RF5.5b
cade forget --person NOMEapaga tudo sobre uma pessoa ecade export --person NOMEexporta 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. Oforgetlista 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 forgettermina 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 --backupslista, pergunta (ou--yes) e as apaga, nunca ocade.db. Ocade doctormostra quantas são, a data e a idade da mais antiga e o tamanho, sem contar como aviso (#87). - RF5.6
cade initacha 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ão600. Olha só nomes, nunca conteúdo; sem entrada, fica com os padrões. - RF5.7
cade doctorconfere 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 parapt*, inglês nos outros casos) ouui.language(auto,pt,en); a resposta doasksegue o idioma da pergunta, e os códigos doask --jsonnã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) seguemui.date_order(auto,dmy,mdy);autolê mês primeiro com o sistema (LC_ALL,LC_TIME,LANG) emen_USe dia primeiro nos outros casos. As datas da saída são sempre ISO (AAAA-MM-DD).
- 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, pasta700), inclusive bancos criados por versões anteriores, que eram644. - RNF1.5 Apagar é definitivo no disco:
secure_deletezera texto apagado ou substituído, ecade forgetcompacta o banco e esvazia o WAL. Oforget fileapaga também o histórico de edições. - RNF1.6
PRIVACY.md/PRIVACY.pt-BR.mddocumentam 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.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ão600). Um banco de versão mais nova que ocadeé 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_hashpara reaproveitar o vetor de textos idênticos (dado derivado, sem cópia); 4 = um evento por arquivo, versões emfile_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 paracade reindex(marcado como pendente), e o banco é compactado no fim; 6 = índice FTS5chunks_ftssobre os pedaços (dado derivado, sem cópia); 7 = índice de pessoasevent_peoplee colunaevents.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 reindexrecalcula 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);ingesteaskrecusam 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.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
versionda 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.1 Adicionar uma nova fonte não exige mudança no modelo de evento nem nas consultas — apenas um novo
EventCollectorregistrado emcmd/cade/sources.go.
- 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 benchmede latência e memória;bench/baseline.txtguarda a referência (RTX 3060, 2026-09-27) ebench/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 asknã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.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.jsontraz 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.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 evalPlanroda 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 evalScalegera a curva por tamanho do corpus (bench/retrieval-scale.txt); o baseline antes da próxima versão fica embench/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 respondeSEM_INFORMACAO; falta de citação é relatada sem reprovar, porque o modelo de 3B às vezes não cita mesmo sem injeção.
- RNF8.1 Cada push e pull request passa por
gofmt,go vet,golangci-lint(errcheck,staticcheck,unused,ineffassign) ego tool mage testna CI (.github/workflows/ci.yml); qualquer falha deixa a execução vermelha.go tool mage checkroda 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.
| 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 alle umcade askque carrega os dois modelos funcionaram, com a resposta e a fonte certas.
| 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 |
| 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 comparadoridb_cmp1do Chromium.
- 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 --captionsnã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.
- 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.
