ADR 0113 — Pasta local anexada vira chunks.scope = 'local', lida pelo NAVEGADOR, nunca um caminho do host
Status
Aceito.
Referencia (sem editar): ADR 0072, ADR 0079, ADR 0080, ADR 0107, ADR 0108.
Contexto
O pedido
Deixar o usuário anexar a um projeto uma pasta local da própria máquina como
material de REFERÊNCIA SOMENTE LEITURA para os agentes — explicitamente SEM
rodar a CLI brabo-runner (ADR 0103). Isto NÃO é execução de projeto nem
workspace: o execution_mode: runner (ADR 0104) existe justamente porque a
execução precisa de um caminho REAL do HOST para o qual o container/engine
possa rotear comandos, confirmado por um runner que se conecta e o verifica
(RN-423). Esta feature está mais perto de "anexar arquivos a uma conversa" do
que de "montar um workspace".
O que o ADR 0072 e o ADR 0107 de fato recusaram, e por que isto é diferente
As duas decisões tratam de resolver um CAMINHO absoluto DO HOST a partir do navegador. O ADR 0072 recusou um seletor de pasta na criação do projeto porque "um seletor exigiria que a api enumerasse para o navegador o sistema de arquivos do CONTAINER" — superfície nova só por ergonomia, e ainda por cima um sistema de arquivos do contexto errado. O ADR 0107 resolveu a navegação de pasta roteando a leitura pelo Runner (um processo com os privilégios do próprio usuário na própria máquina dele, não a api) justamente porque a api nunca enxerga o sistema de arquivos real do usuário.
As duas tratam de DE ONDE VEM UM CAMINHO NO DISCO DO USUÁRIO, e as duas
importam porque um caminho alimenta ou a criação do projeto
(workspace_path) ou a execução de comando (o cwd do TerminalExecutor) —
contextos em que um caminho ERRADO é problema de segurança e de correção: ele
decide contra o que um agente pode executar.
Esta feature nunca resolve um caminho do host. O <input webkitdirectory> do
navegador devolve objetos File; o código lê o conteúdo deles
(File.text()) e o caminho relativo dentro da pasta escolhida
(File.webkitRelativePath) — nunca um caminho absoluto, porque a File API
do navegador não expõe um, por desenho, para site nenhum. O que atravessa a
rede é TEXTO que o navegador já tinha direito legítimo de ler, exatamente o
mesmo modelo de confiança de qualquer upload comum por <input type="file">
em qualquer site. Não há caminho a validar, isolar ou rotear para um runner,
porque não há caminho nenhum. Construir um repasse no estilo do runner para
isto seria resolver um problema que esta feature não tem.
Por que reusar o pipeline do RAG em vez de um mecanismo paralelo
O produto já tem um pipeline de chunking/embedding/busca híbrida que
funciona (ADR 0079/0080): a tabela chunks, o RagEmbeddingService,
chunkText/chunkMarkdownDocument, ChunkRepository.searchByVector/
searchByLexicalQuery, e a renderização de citação da aba Chat RAG
(RagCitationCard, origemDoChunk). Um arquivo anexado localmente tem,
estruturalmente, a mesma forma de um arquivo de doc/ADR indexado: um caminho
mais conteúdo que deve ser buscável e citável. Construir um segundo
armazenamento paralelo de "arquivos anexados" e uma segunda UI de citação
duplicaria um pipeline que já faz exatamente esse trabalho, sem outro motivo
além de a origem dos bytes ser um upload do navegador em vez de um blob do
git.
Decisão
chunks.scope ganha um quarto valor, 'local', acrescentado via ALTER TYPE ... ADD VALUE (migration 0052) — aditivo, sem migração de dados, sem
CHECK novo: as constraints existentes chunks_session_id_casa_com_escopo/
chunks_source_path_casa_com_escopo (migration 0045) já estão escritas
como "é session ou não", então local cai do mesmo lado de docs/adr
(tem source_path, não tem session_id) com zero mudança de schema além do
valor do enum.
IndexLocalFolderUseCase espelha de perto o IndexProjectDocsUseCase:
divide cada arquivo em chunks (ciente de Markdown via
chunkMarkdownDocument para .md/.mdx, chunkText nos demais), chama o
mesmo RagEmbeddingService, grava chunks com embedding: null quando o
embedding falha (o mesmo padrão de honestidade que docs/adr já usam —
RN-233), e faz deleteByScope(projectId, 'local') completo + recriação a
cada chamada. origemDoChunk precisou de ZERO mudança: chunks local
carregam sourcePath exatamente como docs/adr, então caem no ramo
existente kind: 'file' — a UI de citação já os trata.
Recusa de uma vez, nunca truncamento silencioso. Ao contrário de
docs/adr (uma varredura em segundo plano sem usuário olhando), anexar uma
pasta é um gesto único do usuário, com um seletor de pasta na frente dele. O
IndexLocalFolderUseCase RECUSA (400) o upload inteiro quando os tetos
agregados são ultrapassados (contagem de arquivos, soma de bytes) — quem
clicou em "Anexar" está olhando a tela e pode escolher uma pasta menor. Um
arquivo individual grande demais ou com extensão não reconhecida é só
PULADO (filesSkipped), nunca derruba o lote — a mesma distinção que o
IndexProjectDocsUseCase já faz implicitamente ao filtrar só .md.
Nunca ligado ao "Reindexar agora" genérico. O ReindexProjectUseCase
relê de uma fonte que o SERVIDOR pode revisitar — o repositório do projeto
(docs/adr) e o event log (session). O escopo local não tem fonte
assim: o texto vive só no navegador de quem o anexou, e o servidor nunca
persistiu um caminho do host (não existe um). Chamar deleteByScope(projectId, 'local') pelo botão genérico de reindexar apagaria o material de referência
anexado pelo usuário sem forma de recriá-lo — reenviar a pasta é o ÚNICO
mecanismo de ressincronização, e ele é um botão deliberadamente separado
(POST .../rag/local, maintainer, o mesmo papel de reindex porque ele
também aciona o provider de embedding e substitui o que está indexado). Os
dois casos de uso levam um comentário explícito no código apontando um para o
outro e para este ADR, para que uma futura "correção" não os ligue e apague
em silêncio o material de referência dos usuários num clique sem relação.
A cobertura diz a verdade sobre o que local de fato é.
RagCoverage.local é uma forma NOVA (RagLocalCoverage), não um reuso de
RagFileCoverage: não há "total de arquivos no repositório" com que
comparar — um upload do navegador não tem repositório a revarrer. Ela
informa o que está indexado AGORA (filesIndexed, folderName,
lastAttachedAt). lastAttachedAt é a ÚNICA exceção declarada à regra do
produto de "nunca mostrar um timestamp chutado" (RN-237/ADR 0080): é um
MAX(chunks.created_at) real sobre o escopo, não um valor calculado ou
estimado — a mesma disciplina de honestidade, aplicada a um escopo em que um
timestamp real por acaso tem resposta.
O que este ADR NÃO faz
- Não toca
execution_mode/workspace_path/o runner. Eles continuam exatamente como os ADRs 0072/0104/0107 descrevem: como o código de um projeto EXECUTA. Esta feature acrescenta material de referência ao índice do RAG; não tem opinião sobre onde o código de um projeto mora ou roda. - Não acrescenta um quinto conceito de "arquivos anexados". Nenhuma
tabela nova, nenhum formato de citação paralelo —
localé um valor do mesmoChunkScopeque o resto do pipeline já fala. - Não indexa o repositório do próprio projeto uma segunda vez.
docs/adrcontinuam sendo lidos do repositório git do projeto viaReadProjectCodeUseCase;localé deliberadamente uma fonte SEPARADA, para material que não tem nada a ver com o repositório do próprio projeto (uma spec, uma implementação de referência, a doc de um concorrente, qualquer coisa que o usuário queira que um agente consiga buscar).
Consequências
- Um segundo upload substitui o primeiro, inteiro. Não há "acrescentar
ao que está anexado" — o
IndexLocalFolderUseCaseapaga o escopolocalinteiro antes de gravar o novo. É a MESMA disciplina de reconstrução completa e idempotente quedocs/adr/sessionjá seguem (RN-231), aplicada de forma coerente em vez de inventada especialmente para este escopo. - Arquivos binários e grandes demais ficam ausentes da busca em silêncio,
declarados via
filesSkipped. A allowlist de extensões (RAG_LOCAL_ALLOWED_EXTENSIONS) é uma restrição real de conteúdo, não só acabamento de UX — o servidor a impõe independentemente do que o navegador tenha pré-filtrado, porque o pré-filtro do lado do cliente é conveniência, não a garantia. - A fronteira de confiança é exatamente a de um upload de arquivo comum.
Sem isolamento, sem allowlist de caminho, sem ticket de runner — porque não
há caminho nenhum nesta feature, só bytes que o navegador já tinha. Quem
pode chamar
POST .../rag/local(papelmaintainer) pode pôr texto arbitrário no índice de busca do projeto; é a mesma autoridade que a rota já exige parareindex.
Alternativas consideradas
- Rotear a leitura pelo Runner (ADR 0103/0107), como a navegação de pasta. Recusada: o Runner existe para dar ao ENGINE um caminho real do host contra o qual executar comandos, numa máquina que o usuário configurou explicitamente com a CLI. Esta feature foi pensada explicitamente para funcionar SEM a CLI, para material de referência somente leitura — roteá-la pelo Runner tornaria a CLI um requisito que esta feature foi desenhada para não ter.
- Uma tabela
attachmentsnova, independente dechunks. Recusada: duplicaria chunking, embedding, busca híbrida e renderização de citação sem benefício nenhum — a forma "caminho + conteúdo, buscável, citável" é exatamente o quechunksjá modela. - Ligar
localaoReindexProjectUseCase. Recusada: não há nada do lado do servidor a reler para este escopo; ligá-lo faria o botão genérico de reindexar apagar em silêncio o material de referência anexado pelos usuários, sem forma de trazê-lo de volta.