Pular para conteúdo

AUR-594: PR grande revisada (diff local quando a API recusa, lotes quando passa do orçamento)

Medido antes (base e81f63c5)

  • PR #88 (intervalo 9038e70e...a7f2b0a6, origin/card/AUR-589): 117 arquivos, 1565 inserções e 30562 remoções. A API do GitHub recusa o diff acima de 20000 linhas com HTTP 406 e o código too_large; githubclient.GetPullRequestDiff devolvia HTTP 406: ... como erro comum e fetchPullRequest saía 1 (fetching pull request diff: HTTP 406 ... too_large): nada revisado.
  • AUR-593 (996732f0..8c7fcdc8, 111 arquivos, --base): 6 arquivos ficaram fora do prompt pelo limite de tokens (prompt único de 120000) e o gate ficou partial_coverage.

O que mudou

  • Recusa tipada (internal/git/githubclient/diff_too_large.go, client.go): só 406 com "too_large" no corpo vira ErrDiffTooLarge; outro 406 continua erro comum. Nenhum dos dois devolve diff. ParseUnifiedDiff expõe o mesmo parser do diff da API; PullRequestMetadata.BaseSHA lê base.sha.
  • Diff local (internal/analyzer/rangediff.go, cmd/aurumcode/pr_local_diff.go, review_pr_inputs.go): na recusa, o checkout é verificado antes de qualquer leitura (repositório e head do PR, codebaseContextMismatch; árvore limpa, verifiedCleanCheckoutReason). O head é o HEAD verificado; a base é AURUMCODE_BASE_SHA quando é um commit do checkout, senão base.sha da API. Repo.RangeDiff roda git diff base...head (base de merge, janelas de contexto como a API, --no-ext-diff --no-textconv, core.fsmonitor=false) e o texto passa por ParseUnifiedDiff: mesmos caminhos, lados e hunks do diff da API; binário sai sem hunk e segue a classificação de sempre do --pr. Sem git (internal/analyzer/mergebase.go, windowed.go): a base de merge é lida do banco de objetos (os ancestrais comuns que não são ancestrais de outro comum; zero ou mais de um, como num histórico cruzado, é erro, nunca um palpite) e os hunks são montados com as mesmas janelas de 3 linhas; TestRangeDiffFromObjectsMatchesGit prova arquivo a arquivo e hunk a hunk que o resultado é o do git diff base...head, inclusive com a base andando depois do ponto de ramificação. Base ausente ou checkout não verificado: saída 1, nada enviado nem publicado.
  • Parser de hunk (githubclient.parseHunkHeader): o cabeçalho de um arquivo de uma linha apagado (@@ -1 +0,0 @@, que a API e o git emitem) derrubava o processo (slice bounds out of range); agora um lado sem contagem vale uma linha e um cabeçalho malformado dá zeros (TestParseHunkHeaderWithoutCount).
  • Diff local sempre? Avaliado e recusado: a CLI direta e os testes com GitHub falso não têm checkout verificado, então "sempre local" trocaria uma revisão que hoje funciona por uma falha; e com a API respondendo, o diff dela é a fonte que o PR mostra. O local é só o caminho da recusa por tamanho.
  • Lotes (internal/review/batches.go, batch_merge.go, reviewer.go): GenerateReviewWithContext monta o prompt único como antes; só quando ele deixaria um arquivo com patch omitido ou parcial (arquivo sem patch não conta) a revisão planeja lotes: arquivos agrupados por diretório, diretório inteiro num lote quando cabe, senão o lote é completado arquivo a arquivo; arquivo grande demais sozinho é um lote próprio (parcial, declarado). Cada lote passa pelas mesmas quatro etapas (reviewPrepared) com a evidência dos seus arquivos (ids atribuídos uma vez, os mesmos em todos os lotes). Os resultados são consolidados: listas concatenadas, contagens somadas, avisos juntados, flag ligada continua ligada, veredito mais severo; um gate. Transcrições de deliberação são somadas. Um lote que falha faz a revisão falhar. O prompt único que cabe fica byte a byte igual.
  • Tetos (internal/prompt/templates/limits.yml: batch_max_count: 4, batch_max_prompt_tokens: 480000; internal/config/batches.go: seção batches com max_batches e max_prompt_tokens, validada, linha em governedSections, política decide sozinha). A partir do primeiro lote que passa de um teto, nenhum lote é revisado: os arquivos vão para code_files_omitted e code_files_omitted_paths, o aviso de cobertura os lista (passes.go, review_coverage.go), a auditoria os põe em coverage.omitted_files, e a cobertura parcial retém a aprovação.
  • Auditoria (internal/render/audit.go, cmd/aurumcode/review_batches.go, compliance_artifacts.go, review_evidence.go): campo batches (índice, arquivos, tokens estimados); stderr reviewed in N batches by directory (files per batch: ...).
  • Tutorial revisao, caso pr-workflow: GitHub falso (_lib/github-falso.py aceita 406 e BASE; _lib/pr.sh ganha tut_pr_servidor_recusa) com três passos: 406 + diff local + 3 lotes (exit 0, APPROVE); teto batches.max_batches: 1 (exit 1, partial_coverage, 4 arquivos listados); checkout sujo (exit 1, nada publicado). O caso ficou dentro de pr-workflow (um caso novo exigiria registro em docs/specs/AUR-561.md, fora de paths).
  • Docs: configuration.md (seção "PR grande" e tabela), getting-started.md, architecture.md, tutorials/revisao.md.

Prova

Aceite (tests/acceptance/AUR-594.sh, go-shared)

Item Teste Última linha
AC-001 checkout montado sem git (objetos escritos em Go): TestTooLargeDiffIsReadFromObjectCheckout (o prompt carrega o arquivo novo e +\treturn 2) e TestTooLargeDiffWithUnverifiedObjectCheckoutFails (arquivo não versionado: exit 1, nada enviado nem publicado); no selado (sem git) elas exercem o leitor do banco de objetos; com git (go-shared) também TestTooLargeDiffIsReadFromTheVerifiedCheckout, TestTooLargeDiffWithUnverifiedCheckoutFails (repositório real) e TestRangeDiffFromObjectsMatchesGit (paridade dos dois leitores); TestDiffRefusedAsTooLargeIsTyped, TestParseHunkHeaderWithoutCount AUR-594/AC-001/pass (git and object database); no selado, (object database; no git binary here)
AC-002 TestDiffAboveTheBudgetIsReviewedInBatches (6 arquivos grandes em 3 diretórios: reviewed in 2 batches (4, 2), exit 0 sob block, auditoria com batches, "complete": true, "decision": "pass") AUR-594/AC-002/pass
AC-003 TestBatchCeilingLeavesFilesOutAndWithholdsApproval (max_batches: 1 e max_prompt_tokens: 120000: exit 1, partial_coverage, c/one.go e c/two.go listados, auditoria "complete": false) AUR-594/AC-003/pass
AC-004 caso do tutorial gravado e esta spec AUR-594/AC-004/pass
MUT-001 406 vira diff vazio (return &Diff{}, nil): TestTooLargeDiffIsReadFromObjectCheckout vermelho AUR-594/MUT-001/rejected
MUT-002 declareOutside não declara nada: exit=0, want 1 with partial_coverage and no approval AUR-594/MUT-002/rejected
MUT-003 zero lotes admitidos consolidando um resultado vazio (if len(plan.batches) < 0): TestNoBatchAdmittedIsNeverApproved vermelho AUR-594/MUT-003/rejected

Testes de unidade: TestPackUnitsKeepsDirectoriesAndFillsBatches, TestMergeResultsConsolidatesBatches, TestFilesWithoutPatchRideInTheFirstBatch.

AC-004 na imagem do produto: a PR #88 deste repositório

Imagem aurum-tutoriais:aur594 construída deste worktree. Clone do repositório com HEAD em a7f2b0a6 (head da PR #88), origin = https://github.com/Mpaape/AurumCode.git; GitHub falso local (github-falso.py PORTA 406 LOG a7f2b0a6... 9038e70e...) recusando o diff; modelo de fixture com aprovação limpa; review --pr 88 --repo Mpaape/AurumCode --publicar --modo-publicacao review --politica /policy --auditoria ....

Com a configuração do repositório como política (ignore: tests/**, docs/assets/capturas/**, gate fail_on: [error], inconclusive: block):

aurumcode review: the API refused the pull request diff as too large; reviewing the same range computed from the verified checkout (9038e70e6d38...a7f2b0a63e4e, 117 file(s))
review formal "APPROVE" publicado no pull request #88 (0 comentário(s) na linha).
rc=0
audit: "gate": {"decision": "pass"}, "coverage": {"complete": true, "omitted_files": [113 ignorados]}

Sem ignore (os 117 arquivos, 30 mil linhas removidas, só o gate):

aurumcode review: the API refused the pull request diff as too large; reviewing the same range computed from the verified checkout (9038e70e6d38...a7f2b0a63e4e, 117 file(s))
aurumcode review: the diff did not fit one prompt; reviewed in 4 batches by directory (files per batch: 27, 55, 24, 11)
review formal "APPROVE" publicado no pull request #88 (0 comentário(s) na linha).
rc=0
audit: {"decision": "pass"}, "complete": true, omitted 0
batches: (1, 27 arquivos, 117599 tokens) (2, 55, 119858) (3, 24, 107352) (4, 11, 70482)

Medido no caminho: com o padrão de 400000 tokens o quarto lote passava do teto e 11 arquivos ficavam fora (rc 1, partial_coverage, listados); o padrão virou 480000 (quatro prompts cheios), e o número de lotes é o teto que manda. Antes disso o empacotamento fechava o lote a cada diretório que não cabia (lotes de 5, 22, 43 e 29 arquivos, o primeiro com 14930 tokens); agora o lote é completado arquivo a arquivo.

O intervalo do AUR-593 (996732f0..8c7fcdc8)

--base 996732f0 na imagem, mesma política:

aurumcode review: the diff did not fit one prompt; reviewed in 2 batches by directory (files per batch: 72, 36)
Cobertura da revisão — 99 de 111 arquivo(s) do diff foram cobertos; 4 não foram revisados por completo.
- 4 arquivo(s) foram filtrados antes da revisão (gerado, grande demais, sem patch ou binário fora do catálogo de formatos)
rc=1

Nenhum arquivo fora pelo orçamento (eram 6); o parcial que resta são os 4 gerado.go dos tutoriais, gerados de propósito (legítimo). No --pr com o diff local o mesmo intervalo cabe num prompt (janelas de contexto, não o arquivo inteiro do --base): sem lotes, parcial só pelos gerado.go e pelas PNG apagadas (sem patch). Medido também: antes de tirar os arquivos sem patch da decisão, cada PNG apagada ocupava um lote próprio.

Regressão

  • go vet ./... && go test -p 1 ./... (go-shared): rc 0, nenhum pacote falhou.
  • Aceites (go-shared), última linha: AUR-594/all/pass, AUR-576/all/pass (depois de tirar o t.Setenv("PATH") dos testes deste card: a guarda do AUR-576 conta essas chamadas), AUR-578/all/pass, AUR-593/all/pass, AUR-575/all/pass, AUR-588/all/pass; AUR-561, 562, 563 e 564 rc 0 (AC-002-MUT-001/ok (sem a linha que prova o caso, --check diverge em todos os casos)). O AUR-561 copia só _lib/tutorial.sh para as cópias da mutação: o run.sh da revisão carrega _lib/pr.sh só dentro do caso do PR grande.
  • Tutoriais: os 18 run.sh all em paralelo com aurum-tutoriais:aur594 (todos rc 0) e --check de cada um: 18 CHECK OK. Os commits do PR grande do tutorial têm data fixa, então os SHAs da saída são estáveis.
  • bash scripts/docs/capturas.sh rc 0 e bash scripts/docs/build.sh com 0 warnings.
  • Selado: oci-run --profile go-unit-offline-v1 --card AUR-594, exit_code: 0 (sem git no perfil: o AC-001 exerce o leitor do banco de objetos).
  • Teto e cache: TestBatchCeilingLeavesFilesOutAndWithholdsApproval roda a revisão duas vezes no mesmo diretório de cache: a revisão parcial não é gravada no cache por arquivo, e a segunda continua exit 1 com os mesmos arquivos fora.
  • CI real: o review publicado na PR #91 (AUR-595) não traz o aviso "Contexto do repositório omitido", isto é, o checkout do workflow (.aurumcode-target, working-directory da revisão) passa na verificação de identidade e limpeza; é o mesmo pré-requisito do diff local.

Decisões e pendências

  • O caso do PR grande vive dentro de pr-workflow do tutorial revisao: um caso novo exigiria registro em docs/specs/AUR-561.md, fora de paths.
  • runProfilePasses (com review.profiles) monta revisores com os tetos padrão: a seção batches não chega a eles.
  • Revisão (REQUEST_CHANGES, corrigido): com max_prompt_tokens menor que um prompt nenhum lote era planejado, o modelo nunca era chamado e mergeResults([]) dava um resultado vazio que formalReviewEvent lê como APPROVE (exit 0 sem gate). Agora zero lotes é review.ErrNoBatchReviewed, que embrulha llm.ErrAllProvidersFailed: o mesmo caminho de uma revisão de modelo que não rodou (--base: saída 1; --pr sem gate: saída 1; com gate: quality_degraded, veredito nunca approve, gate inconclusivo). TestNoBatchAdmittedIsNeverApproved (max_prompt_tokens: 1, sem gate): exit 1 e sem "Approve"; MUT-003 (tirar o teste de zero lotes) o deixa vermelho (AUR-594/MUT-003/rejected). canonicalVerdict não mudou: ele deriva do resultado (achados, quality_degraded, gate retido), não do Verdict do modelo, e um resultado limpo sem Verdict é a aprovação legítima de toda revisão sem achados (os tutoriais e fixtures dependem disso); o conserto é nunca produzir um resultado de modelo sem modelo.

  • O aviso de cobertura dos arquivos fora pelo teto reaproveita a frase "left out of the review by the token budget" (o teto de lotes é orçamento de tokens); a lista nomeia cada arquivo.

  • Teto global de custo: batches.max_prompt_tokens já é a soma dos prompts de todos os lotes, e --limite usa um único rastreador de custo para todas as chamadas da revisão (o orquestrador é um só). Pendente: só deliberation.max_cost_tokens vale por lote (uma sessão de deliberação por lote); uma revisão em quatro lotes pode gastar até quatro vezes esse teto.
  • O veredito reaproveitado (verdict-reuse) não inclui os tetos de lotes na chave: uma revisão completa é a mesma com qualquer teto, e uma parcial não é reaproveitada.