Pular para o conteúdo principal

ADR 0148 — Os artefatos dos agentes projetados em arquivo, numa pasta docs/

Context​

Todo artefato que os agentes produzem — o brief do Criativo, as regras de negócio do PO, o diagrama C4 e o roteamento de módulos do Arquiteto, as decisões dos seis conversacionais — vive exclusivamente no event log. Engine.Harness.Tools.EmitArtifact (apps/engine/lib/engine/harness/tools/emit_artifact.ex:3-5) é explícito: "não há tabela de artefatos". O artefato É o evento artifact.<tipo>, imutável, com autor e seq.

Isso é a decisão certa para a FONTE, e não se mexe nela aqui. O problema é outro: essa memória não chega a lugar nenhum que uma pessoa abra. Ela é legível pela timeline da sessão, uma linha por vez, dentro do produto. Quem abre a pasta do projeto no editor — que é onde o código vai estar — não encontra nada do raciocínio que produziu aquele código.

O buraco ficou maior com a decisão de adiar o git (provisionamento adiado, em PR próprio): entre a criação do projeto e o handoff do Arquiteto ao Dev Lead, o projeto existe sem repositório nenhum. Nesse intervalo, tudo o que o produto acumulou sobre o projeto está em linhas de banco, e a pasta do usuário está vazia.

Existe um precedente exato no produto para resolver isso, e ele já foi discutido e aceito: o grafo Neo4j (ADR 0099/ADR 0101), que é declaradamente memória derivada do event log — nunca fonte, podendo ser descartada e reconstruída. GraphProjector (apps/api/src/application/graph-projection/graph-projector.ts) drena outbox_events sob um aggregate_type próprio e escreve num destino externo, degradando sem nunca derrubar quem o alimenta.

A pergunta desta decisão não é se a pasta deve existir — o dono do produto pediu, literalmente, "criar apenas uma pasta por ora chamada docs, onde os artefatos estarão de cada um dos agentes". É com que mecanismo, e o que isso obriga a garantir.

Decision​

A pasta docs/ é uma PROJEÇÃO do event log, no mesmo sentido estrito que o ADR 0101 deu ao grafo. A fonte continua sendo o evento; a pasta pode ser apagada inteira e reconstruída; e nada no produto lê dela para decidir coisa alguma. Ela existe para ser aberta por uma pessoa.

Disso decorre tudo o mais.

1. O mecanismo é o do GraphProjector, peça por peça​

ArtifactProjector (apps/api/src/application/artifact-projection/artifact-projector.ts) copia a forma que já resolveu este problema: poller próprio (setInterval com .unref(), limpo em onModuleDestroy), drainOnce() público e independente do timer (é o ponto de entrada dos testes), flag draining contra ciclos sobrepostos, lote de 50, e markProcessed somente após sucesso.

Não é reuso de código, é reuso de desenho — os dois projetores escrevem em destinos sem nada em comum. Copiar a forma é o que faz um leitor que conhece um entender o outro sem reler.

2. Um aggregate_type próprio, artifact_projection​

AppendSessionEventUseCase grava uma terceira linha de outbox, na MESMA transação, para os tipos em ARTIFACT_PROJECTABLE_EVENT_TYPES. O aggregate_type é novo pelo mesmo motivo que o do grafo (documentado em domain/graph/graph-projection-events.ts:7-26): Engine.Outbox.Drain.run_once/0 só drena session, task e container, e a linha 'session' que todo evento já tem é marcada processed_at por ele a cada ~2s. Ler o mesmo aggregate_type seria correr contra o dreno do engine e perder quase sempre.

Uma diferença deliberada em relação ao grafo: o aggregateId é o projeto, não a sessão. A pasta é do projeto, e o evento de origem só carrega sessionId — gravar o projectId aqui poupa o projetor de uma consulta por artefato para descobrir algo que o caso de uso já tinha em mãos.

3. Lista de PERMITIDOS, e quatro tipos deliberadamente fora​

ARTIFACT_PROJECTABLE_EVENT_TYPES (apps/api/src/domain/artifacts/artifact-projection-events.ts) é lista de permitidos, e não de excluídos: tipo de artefato novo nasce fora da projeção e entra quando alguém decidir que ele é documento — em vez de aparecer sozinho numa pasta que o usuário lê.

Ficam de fora qa_verdict, secops_verdict, task_blocked e infra_delegation_files. São desfechos operacionais, não documentos do produto: um veredito de gate por task, num projeto com dezenas de tasks, encheria docs/qa-lead/ de arquivos que ninguém abre, e a pasta perderia a propriedade que a torna útil — caber numa olhada. Eles continuam no event log, que é a fonte, e continuam na timeline.

4. Por agente, e o versionado sobrescreve​

O layout é docs/<agente>/<arquivo>.md. Navegar a pasta responde "o que o Arquiteto produziu?" sem abrir nada.

Os quatro tipos versionados (module_map, module_routing, project_image, c4_diagram — os que são lidos por redução ao maior version, ver obter-container-do-projeto.use-case.ts:22-25) escrevem sempre o mesmo arquivo, sobrescrevendo: a pasta mostra o VIGENTE, e o histórico continua inteiro no event log. Projetá-los como arquivo por versão produziria c4-diagram-v1.md, c4-diagram-v2.md e faria quem abre ter de descobrir qual vale — exatamente o trabalho que a redução por version existe para poupar.

Os demais são append-only por natureza (uma decisão não substitui a anterior: as duas aconteceram) e cada um vira arquivo próprio, com o seq do evento no nome. O seq é gapless por sessão e já ordena o event log, então é o desempate que não exige estado novo — e sem ele dois artefatos do mesmo tipo com o mesmo título gerariam o mesmo arquivo, e o segundo apagaria o primeiro: a única forma de esta projeção perder informação que a fonte tem.

5. Duas barreiras contra o filesystem, e a segunda não é redundante​

O nome do arquivo é derivado do título que o modelo escreveu — entrada não confiável em toda a extensão do termo. A defesa não é escapar o que veio, é derivar um nome novo de um alfabeto fechado: slugDeArquivo não tem caminho de retorno em que um separador sobreviva, e devolve null quando não sobra nada (nome vazio colidiria com todos os outros nomes vazios).

FsArtifactFileStore checa de novo, com relative(), antes de abrir o arquivo. Não é paranoia: as duas respondem perguntas diferentes — a primeira é "que nome eu derivo disto", a segunda é "o que eu vou de fato abrir". Confiar só na primeira faria a segurança desta escrita depender de nenhuma futura mudança de slug jamais deixar passar um separador, que é precisamente a forma de contenção que o ADR 0130 recusa por princípio: a que depende de o chamador estar correto.

6. Em modo runner, a pasta desvia para a raiz gerenciada​

pastaDeArtefatosDoProjeto (apps/api/src/infrastructure/filesystem/project-workspaces-root.ts) é irmã de permissionsFilePath e faz o mesmo desvio, pela mesma razão física: quem escreve é a api, de dentro do container dela, e um projeto runner é deliberadamente sem bind-mount. O canal do runner (exec/exec_result) só transporta COMANDO — não existe primitiva de escrever conteúdo em arquivo —, então nem delegar a escrita resolveria sem inventar um mecanismo novo.

7. A escrita NÃO passa por proposed_action​

E não é omissão. proposed_action é a origem de todo efeito externo de um agente pedindo para agir. Aqui não há agente pedindo nada: o artefato já foi emitido e já está no event log quando esta classe roda. O que acontece depois é o sistema materializando, num caminho derivado, uma cópia legível de algo que já é fato.

Fazer a projeção passar pela fila de aprovação teria o efeito oposto ao pretendido: encheria a fila de decisões rotineiras — uma por artefato — e corroeria justamente o teto que dá sentido ao clique nas ações que são efeito externo.

Consequences​

O usuário passa a ter, na pasta do projeto, uma pasta docs/ organizada por agente com o raciocínio que produziu o código. Ela aparece antes do git existir (o provisionamento adiado), que é justamente o intervalo em que hoje não há nada em disco.

A projeção nunca derruba a fonte. Nenhum throw escapa do drainOnce: cada item falha isolado, fica logado e permanece processed_at IS NULL para o ciclo seguinte. Disco cheio, permissão negada ou pasta inalcançável não podem impedir um artefato de ser EMITIDO. outbox_events não tem attempts nem dead-letter — o retry é implícito e infinito, exatamente como o do grafo.

Diferença deliberada em relação ao GraphProjector: não há aqui um equivalente ao GraphUnavailableError que pare o lote inteiro. Uma falha de escrita costuma ser do ITEM (um nome estranho, um projeto cuja pasta sumiu), e não do destino; parar o lote por causa de um item bloquearia todos os que estivessem atrás dele.

Evento que sumiu do event log é marcado processado, não retentado para sempre — retentar o que não existe mais não teria efeito nenhum, e a linha ficaria na fila indefinidamente.

Custo declarado 1: em modo runner, docs/ não fica ao lado do código. Quem abrir a pasta do projeto na própria máquina não vai encontrá-la; ela está em <PROJECT_WORKSPACES_ROOT>/<workspace_dir_name>/docs/. É uma perda REAL neste modo, porque a pasta existe justamente para ser aberta junto do código. O que ela evita é a alternativa pior: a projeção simplesmente não acontecer, ou falhar em silêncio a cada artefato.

Custo declarado 2: o Markdown é genérico. O arquivo tem um cabeçalho (tipo, agente, quando, seq) e o payload como bloco JSON. Não há renderizador por tipo — seriam treze lugares para envelhecer quando um schema mudasse, e o que esta pasta precisa entregar é conteúdo legível e rastreável, não diagramação. Um formato mais rico por tipo é decisão própria, quando houver uma leitura real pedindo por ela.

Custo declarado 3: a pasta pode divergir do event log entre um ciclo e outro, e depois de uma falha persistente pode ficar incompleta indefinidamente sem que nada avise. É o mesmo custo que o grafo já aceita, e é tolerável pela mesma razão: a fonte está intacta e a projeção é reconstruível. Não existe hoje comando de reprojeção — apagar docs/ não a reconstrói, porque as linhas de outbox já estão marcadas. É a mesma lacuna que o BRB-018 já registra para o grafo, agora com um segundo consumidor.

O que este ADR NÃO toca. decide.ts e os tetos absolutos; proposed_action como origem de todo efeito externo de agente (ver ponto 7 — a projeção não é um deles); a imutabilidade do event log; os schemas de artefato do engine, que não mudam em nada; e o portão da imagem (ADR 0135).