Pular para conteúdo

AUR-536 — Contexto local do PR só com árvore limpa e prova dos casos inverificáveis

Status: especificação do card AUR-536 (O05-review), follow-up da revisão de AUR-515.

Problema

AUR-515 verifica que o checkout local é, de fato, o repositório revisado no HEAD exato que o PR nomeia, antes de deixar resolveCodebaseContext ler esse checkout. Mas repositório+HEAD corretos não implicam árvore limpa: uma edição não commitada, um arquivo não rastreado, ou arquivos de um clone aninhado continuam sentados no disco e internal/context/resolver.go os envia ao modelo do mesmo jeito, já que seu filepath.WalkDir não tem nenhuma noção de git. Além disso, o branch "inverificável" de codebaseContextMismatch (diretório sem .git, checkout sem origin, falha da API ao buscar o head do PR) nunca foi exercitado pelo --pr real — só a ausência/divergência de repositório e HEAD tinham prova.

Uma primeira versão deste card (SHA 447f9c5) cobriu isso com dois backends próprios em cmd/aurumcode — git ls-tree quando havia binário, existência de objeto loose como fallback sem ele — e falhou a revisão independente em cinco pontos: (B1) o resolver ainda lia .git/zz.go e sub/.git/x.go numa árvore "limpa", porque o próprio enumerate não tinha noção de .git e a verificação e a leitura eram dois caminhos independentes; (B2/B3) o fallback sem git só provava "este conteúdo já foi commitado em algum lugar", não "é o blob commitado NESTE caminho no HEAD" — um git add seguido de reset, ou um conteúdo não rastreado igual a um blob antigo, passava como limpo; (B5) symlinks eram inteiramente ignorados dos dois lados da comparação, então um symlink não rastreado ou reapontado nunca era pego, e o Dropped de um symlink pulado carregava o caminho absoluto do host, não um caminho relativo ao repositório. Este documento descreve o desenho corrigido.

Decisão

A verificação de árvore limpa continua em cmd/aurumcode (verifiedCleanCheckoutReason, aur536.go), só no caminho --pr (pr.go, logo depois de codebaseContextMismatch), nunca no --base, onde o checkout IS a mudança sob revisão por construção (ver o comentário de pacote de aur515.go).

A versão v2 do card passou internal/analyzer para os paths do card exatamente para resolver B2/B3: o leitor de árvore git do projeto (flattenTree/readTree/readCommit) já existia, mas era todo não exportado. Repo.TrackedFiles(ref) (novo, exportado) devolve a árvore de ref já achatada — caminho → {SHA, Mode} — através de QUALQUER backend que OpenRepo tiver escolhido (binário git, que lê objetos soltos ou empacotados via git cat-file; ou o leitor Go puro, só objetos soltos). Não há mais dois caminhos de comparação: aur536.go chama TrackedFiles("HEAD") uma vez e cleanAgainstTracked compara o disco contra esse único mapa, qualquer que seja o backend por baixo. Um objeto ausente — o caso comum sem git num repositório empacotado — é erro de TrackedFiles, nunca um mapa parcial: o chamador trata isso como "inverificável", nunca como "dirty" (B3) — não conseguir provar que a árvore está sujia não é prova de que está.

cleanAgainstTracked (aur536.go) anda a árvore de trabalho uma vez (walkVerified) e, para cada caminho, calcula o id de blob git do seu próprio conteúdo comparável — bytes do arquivo para um arquivo comum, bytes do destino do link (os.Readlink) para um symlink, nunca o conteúdo do que o symlink aponta — e compara contra a entrada do mapa para aquele caminho: presença (ok), symlink-ness (isSymlink == (entry.Mode == "120000")) e o próprio id de blob (B5: antes, um symlink era inteiramente pulado dos dois lados, então um não rastreado ou reapontado nunca era pego). Qualquer uma das três falhando é "dirty". A varredura é limitada em len(tracked)+1 arquivos, nunca uma constante fixa: o próprio git já disse quantos caminhos este commit rastreia, então ver um arquivo além desse número já prova, sem precisar achá-lo pelo nome, que existe um não rastreado — e o tamanho de um repositório real (este, com mais de 2000 arquivos) nunca torna a verificação em si "inverificável". Um gitlink de submódulo (160000) nunca entra no mapa (flattenTree já o descarta: ele nomeia um commit de outro repositório, não um objeto deste) — um submódulo inicializado continua fechando em "dirty"; limitação conhecida, documentada, não corrigida por este card.

Quando a árvore verifica limpa, verifiedCleanCheckoutReason devolve também o conjunto exato de caminhos que TrackedFiles nomeou. resolveVerifiedCodebaseContext (aur536.go) passa esse conjunto para internal/context.Resolver.ResolveWithFiles (novo, resolver.go), que nunca redescobre seu próprio conjunto de arquivos andando o disco: ele lê exatamente os caminhos recebidos, nada além (B1). Isso fecha a lacuna "por construção": antes, a verificação (que pulava .git em qualquer profundidade) e a leitura real do resolver (enumerate, sem noção alguma de .git) eram dois andarilhos independentes que só coincidiam hoje; agora o conjunto que a prova verificou É o conjunto que o resolver lê, o mesmo valor, não dois cálculos que concordam por acaso. enumerate em si também ganhou o mesmo pulo de .git em qualquer profundidade (ainda usado pelo --base, que não tem conjunto verificado para passar), e toda nota de Dropped que ele grava — incluindo a de um symlink pulado — agora nomeia um caminho relativo ao repositório, nunca o caminho absoluto que filepath.WalkDir entrega (B5): uma nota antes carregava o layout de diretórios deste host para dentro do parecer publicado.

codebaseContextReasonUnverifiable (antes um literal repetido três vezes em aur515.go) é uma constante única, e codebaseContextReasonDirty ("dirty") tem um texto de limitação próprio em codebaseContextOmittedNotice, distinto do texto genérico de "inverificável" — necessário para que uma mutação que colapsa um motivo no outro seja pega pelo teste certo, não mascarada por outro caminho que já falha fechado por razões diferentes.

A documentação (docs/getting-started.md, docs/configuration.md) avisa quem monta a Action Docker diretamente (sem o workflow reutilizável): sem um actions/checkout explícito em ref: ${{ github.event.pull_request.head.sha }}, o padrão do actions/checkout num evento pull_request é o merge ref sintético — um commit que não é o head revisado —, e o checkout local diverge do HEAD que a API reporta para o PR: o contexto de codebase sai omitido pelo motivo "head" que AUR-515 já reconhece (não chega a passar pela verificação de árvore limpa deste card, que só roda depois de repositório+HEAD baterem). O workflow reutilizável (.github/workflows/review.yml) já faz esse checkout correto e não muda.

Prova

cmd/aurumcode/aur536_test.go, no mesmo molde httptest+ AURUMCODE_LLM_FIXTURE de aur515_test.go:

  • TestAUR536UntrackedFileOmitsContextFromPrompt (AC-001): um arquivo não rastreado cujo caminho é o marcador do teste.
  • TestAUR536NoGitMetadataOmitsContext, TestAUR536NoOriginRemoteOmitsContext, TestAUR536PullRequestMetadataFailureOmitsContext (AC-002): as três pernas de codebaseContextMismatch que já existiam, mas nunca tinham sido dirigidas pelo --pr real.
  • TestAUR536GitDirectoryFilesNeverReachPrompt (B1): arquivos em .git/<marcador>.go e sub/.git/<marcador>.go numa árvore clean e verificada — nenhum dos dois pode chegar ao prompt, e sub/ok.go, fora do .git aninhado, continua chegando (prova que o pulo é escopado a .git, não a sub inteiro).
  • TestAUR536ModifiedTrackedFileOmitsContext (B4, arquivo rastreado modificado) e TestAUR536StagedButUncommittedChangeOmitsContext (B4, mudança staged — com um objeto loose órfão para o conteúdo modificado, simulando exatamente o que git add/stash deixa no object store): ambas provam que a comparação é contra o blob do HEAD para aquele caminho, não contra "existe algum objeto com este conteúdo" — a vulnerabilidade da v1 deste card.
  • TestAUR536RetargetedSymlinkOmitsContext e TestAUR536UntrackedSymlinkOmitsContext (B5): um symlink rastreado reapontado no disco, e um symlink extra nunca rastreado; antes, os dois passavam como limpos porque symlinks eram ignorados dos dois lados.

internal/context/resolver_test.go ganha três provas no nível do próprio resolver: TestEnumerateSkipsGitDirectoryAtAnyDepth (B1, enumerate em si), TestEnumerateDroppedPathsAreRepoRelative (B5, nenhuma nota de Dropped carrega o caminho absoluto do host) e TestResolveWithFilesRestrictsScanToExactSet (B1: um arquivo que Resolve encontraria sozinho nunca aparece quando ResolveWithFiles recebe um conjunto que o exclui — prova que a restrição é real, não vácua).

tests/acceptance/AUR-536.sh roda os pacotes num clone isolado (go.mod go.sum cmd internal pkg), com seletores all, AC-001, AC-002, AC-002-MUT-001 e AC-001-MUT-002:

  • AC-002-MUT-001 colapsa a constante codebaseContextReasonUnverifiable para "" numa única linha (a declaração), transformando as três pernas "inverificável" de codebaseContextMismatch em "verificado" de uma vez só; as três provas de AC-002 precisam virar RED.
  • AC-001-MUT-002 (novo) remove a cláusula || entry.SHA != blobSHA1(data) da condição de match de cleanAgainstTracked — "larga a comparação de blob" —, então um caminho rastreado presente no disco, com symlink-ness correta, "bate" independentemente do próprio conteúdo; as provas de arquivo modificado, mudança staged e symlink reapontado precisam virar RED.

Verificado manualmente com o binário git presente e com PATH sem git (simulando o perfil selado); os dois backends concordam em todos os casos, inclusive nos testes herdados de AUR-515 (TestAUR515MatchingHeadUsesContext continua passando sem git no PATH).

Verificação adicional, fora da suíte automatizada: um git clone real deste próprio repositório (packed objects, mais de 2000 arquivos rastreados, 8 symlinks/gitlinks — a mesma forma que um actions/checkout produz, não um fixture de objetos loose) confirma verifiedCleanCheckoutReason limpo ("", 2140+ arquivos) com git no PATH, e documentadamente "inverificável" sem git no PATH (objetos empacotados não são lidos pelo leitor Go puro de TrackedFiles — ver Decisão, e é exatamente por isso que B3 exige "inverificável", nunca "dirty", nesse caso). Essa verificação não entra como teste automatizado porque dependeria de clonar o próprio repositório em tempo de teste; o comportamento que ela prova é coberto, caso a caso, pelos fixtures acima.

Correções da segunda re-revisão (SHA ba71981 → atual)

  • Subdiretório do checkout: TrackedFiles devolve caminhos relativos à raiz do repositório, mas walkVerified calculava os seus próprios relativos a partir de dir — se o processo rodasse com cwd num subdiretório de um checkout limpo, nenhum caminho batia, e o resultado era "dirty" publicado sobre um checkout que não tinha nada de errado. verifiedCleanCheckoutReason agora exige que dir carregue sua própria entrada .git (arquivo ou diretório — cobre também um worktree vinculado) antes de confiar nele como raiz; de um subdiretório, devolve "inverificável" em vez de tentar (e errar) a aritmética de prefixo sem git. TestAUR536SubdirectoryCheckoutIsUnverifiable prova.
  • ResolveWithFiles(nil): antes, um files nulo colapsava silenciosamente para o mesmo nil que Resolve usa internamente para dizer "varra tudo" — um conjunto verificado vazio seria lido como "sem restrição". resolve agora recebe um restrict bool próprio, separado do valor de allowedFiles. TestResolveWithFilesNilScansNothing prova.
  • Mutação Mc (TrackedFiles erra → "inverificável"): provado agora por TestAUR536PackedRepositoryWithoutGitIsUnverifiable, que constrói um repositório real com git (binário real, só para montar o fixture) e força git repack -a -d antes de limpar PATH para o código sob teste. Empacotado e sem git, o leitor de objeto loose nunca encontra os objetos — "inverificável", nunca "dirty" nem "limpo" por engano.
  • Mutação Ma (o conjunto verificado é mesmo o que o resolver lê): resolveWithFilesHook (aur536.go), uma variável de pacote que só existe para teste substituir, deixa TestAUR536VerifiedCodebaseContextReadsExactlyTheVerifiedSet capturar os argumentos exatos de uma chamada ResolveWithFiles dentro de um runPRReview real — prova a fiação, não só o resultado (que, numa árvore já comprovadamente limpa, seria idêntico com ou sem a restrição).
  • Paridade AUR-490: resolveCodebaseContext (passes.go, --base) e resolveVerifiedCodebaseContext (aur536.go, --pr) compartilham codebaseContextJSON (passes.go) para a cauda comum — resolver e serializar ou degradar para "" — em vez de duplicar o mesmo marshal. TestAUR490SharedPasses passa a exigir codebaseContextJSON tanto em passes.go quanto em aur536.go.