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 decodebaseContextMismatchque já existiam, mas nunca tinham sido dirigidas pelo--prreal.TestAUR536GitDirectoryFilesNeverReachPrompt(B1): arquivos em.git/<marcador>.goesub/.git/<marcador>.gonuma árvore clean e verificada — nenhum dos dois pode chegar ao prompt, esub/ok.go, fora do.gitaninhado, continua chegando (prova que o pulo é escopado a.git, não asubinteiro).TestAUR536ModifiedTrackedFileOmitsContext(B4, arquivo rastreado modificado) eTestAUR536StagedButUncommittedChangeOmitsContext(B4, mudança staged — com um objeto loose órfão para o conteúdo modificado, simulando exatamente o quegit add/stashdeixa 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.TestAUR536RetargetedSymlinkOmitsContexteTestAUR536UntrackedSymlinkOmitsContext(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-001colapsa a constantecodebaseContextReasonUnverifiablepara""numa única linha (a declaração), transformando as três pernas "inverificável" decodebaseContextMismatchem "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 decleanAgainstTracked— "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:
TrackedFilesdevolve caminhos relativos à raiz do repositório, maswalkVerifiedcalculava os seus próprios relativos a partir dedir— se o processo rodasse comcwdnum 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.verifiedCleanCheckoutReasonagora exige quedircarregue 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.TestAUR536SubdirectoryCheckoutIsUnverifiableprova. ResolveWithFiles(nil): antes, umfilesnulo colapsava silenciosamente para o mesmonilqueResolveusa internamente para dizer "varra tudo" — um conjunto verificado vazio seria lido como "sem restrição".resolveagora recebe umrestrict boolpróprio, separado do valor deallowedFiles.TestResolveWithFilesNilScansNothingprova.- Mutação Mc (
TrackedFileserra → "inverificável"): provado agora porTestAUR536PackedRepositoryWithoutGitIsUnverifiable, que constrói um repositório real comgit(binário real, só para montar o fixture) e forçagit repack -a -dantes de limparPATHpara o código sob teste. Empacotado e semgit, 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, deixaTestAUR536VerifiedCodebaseContextReadsExactlyTheVerifiedSetcapturar os argumentos exatos de uma chamadaResolveWithFilesdentro de umrunPRReviewreal — 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) eresolveVerifiedCodebaseContext(aur536.go,--pr) compartilhamcodebaseContextJSON(passes.go) para a cauda comum — resolver e serializar ou degradar para""— em vez de duplicar o mesmo marshal.TestAUR490SharedPassespassa a exigircodebaseContextJSONtanto empasses.goquanto emaur536.go.