Pular para conteúdo

AUR-540 — O seletor E2E do AUR-467 volta a passar

Problema

tests/e2e/AUR-467.sh E2EAUR467, seletor de um card em done, saía com 1 em main:

AUR-467/E2E/no-regression-gate-exit:0

A revisão do AUR-539 (2026-10-02) já tinha medido que a causa não era orçamento de tokens. Este card mede a causa real e corrige o lado certo sem alterar o que o AUR-467 prometeu (classificação código/prosa e declaração de cobertura, ambas em internal/prompt).

Medição

tests/e2e/AUR-467.sh builda o binário real e roda:

aurumcode review --base HEAD~1 --fail-on error

contra o fixture tests/fixtures/repos/git-demo/repo.git, injetando uma única resposta de LLM canônica via AURUMCODE_LLM_FIXTURE (um achado error/security/command-injection em config/demo-tokens.txt:4). O script espera rc == 3 (exitFindings, cmd/aurumcode/main.go:1316) — o código que --fail-on usa quando um achado da severidade escolhida sobrevive.

Rodando o script sem alteração em main (dentro do container aurum-go via go-shared exec, nunca no host):

$ go-shared exec -w <worktree> bash tests/e2e/AUR-467.sh E2EAUR467
## Code Review Summary
**Verdict:** Approve
...
No issues found.
aurumcode review: 1 finding(s) descartado(s) pelo gate de escopo e evidencia: 1 sem evidencia concreta
AUR-467/E2E/no-regression-gate-exit:0
exit 1

O binário saiu 0 (aprovado, zero achados) em vez de 3. O achado do fixture foi descartado antes de chegar ao --fail-on, pela mensagem "gate de escopo e evidencia" / "sem evidencia concreta".

Essa mensagem vem de internal/review/scope.go:

// filterModelIssues enforces the review's precision contract after parsing
// and redaction, before rule citations or publication. A finding is useful
// structurally publishable when it points to an addition or deletion and
// supplies evidence, impact and a verification proposal.
func filterModelIssues(diff *types.Diff, issues []types.ReviewIssue) ([]types.ReviewIssue, scopeDiscardSummary) {
    ...
    if strings.TrimSpace(issue.Evidence) == "" {
        discarded.MissingEvidence++
        continue
    }
    if strings.TrimSpace(issue.Impact) == "" {
        discarded.MissingImpact++
        continue
    }
    if strings.TrimSpace(issue.Verification) == "" {
        discarded.MissingVerification++
        continue
    }
    ...

internal/review é read_paths neste card, não paths: este gate é produto já fechado e correto, de um card anterior, e não está em escopo para alteração aqui. O achado do fixture do AUR-467 só tinha file, line, severity, rule_id, message, suggestion — nenhum evidence, impact ou verification. pkg/types.ReviewIssue já declara os três campos (json:"evidence,omitempty", json:"impact,omitempty", json:"verification,omitempty"); o fixture nunca os preencheu.

A primeira verificação do gate (se o achado cai dentro de uma linha adicionada/removida do diff, OutsideAddedLines) passou — o relatório só contou 1 ocorrência de MissingEvidence, nenhuma de OutsideAddedLines. Confirma que o achado tinha localização válida e foi descartado especificamente por faltar evidence/impact/verification.

Nem todo fixture de e2e deste repositório ficou atrás do contrato de evidência: tests/e2e/AUR-439.sh já preenche evidence/impact/ verification nos dois achados sintéticos que escreve. Mas tests/e2e/AUR-461.sh — de onde AUR-467.sh herdou o formato do double de LLM — não preenche nenhum dos três, assim como AUR-467.sh antes desta correção; não foi verificado neste card se AUR-461.sh e os demais fixtures de AURUMCODE_LLM_FIXTURE sem esses campos (fora dos paths deste card) também passam a sair RED sob o gate atual — ficam como candidatos a card de follow-up, não corrigidos aqui.

Causa

Não é regressão do produto dentro do que o AUR-467 prometeu. O gate de escopo e evidência (internal/review/scope.go) é comportamento correto e fora do escopo do AUR-467 — ele decide se um achado do modelo é estruturalmente publicável (localização + evidência + impacto + verificação), não se um arquivo é código ou prosa. O fixture do E2E do AUR-467 ficou obsoleto: foi escrito antes desse contrato de forma do achado e nunca ganhou os três campos.

Correção

tests/e2e/AUR-467.sh: o JSON do fixture ($run_dir/fixtures/finding.json) ganhou evidence, impact e verification não vazios, descrevendo a mesma injeção de comando que o achado já alegava — sem citar conteúdo do arquivo de fixture config/demo-tokens.txt e sem reaproveitar o texto das duas asserções já existentes (user input reaches a shell, security/command-injection), para que elas continuem provando que a mensagem e a citação de regra renderizadas vieram do achado, não do texto de evidência:

"evidence": "linha 4 concatena uma variavel de ambiente direto na string de comando",
"impact": "um valor hostil na variavel vira comando arbitrario no shell",
"verification": "reexecutar com um valor contendo ; e confirmar que nao e mais interpretado pelo shell"

Com isso o achado sobrevive a filterModelIssues, chega ao --fail-on error, e o binário sai 3 como o E2E sempre esperou.

Por que preserva a promessa do AUR-467

  • Nenhuma linha de internal/prompt, internal/review ou do mecanismo de classificação código/prosa foi tocada.
  • AC-001/002/003 do AUR-467 (exclusão de regra de código em prosa, cobertura de código mantida, declaração de cobertura parcial) continuam provados por tests/acceptance/AUR-467.sh e tests/unit/AUR-467.go diretamente contra PromptBuilder.BuildPrompt — nenhum dos dois foi alterado por este card.
  • O E2E do AUR-467 nunca foi prova de AC: o próprio cabeçalho do script documenta que é um controle de não regressão da tubulação cmd/aurumcode -> internal/review -> internal/prompt, porque o double de LLM retorna sempre o mesmo JSON independente do prompt. Dar forma válida ao achado do fixture sob o contrato atual de evidência não muda o que esse controle mede.

Mutação cética

MUT-001 (tests/acceptance/AUR-540.sh AC-001-MUT-001): numa cópia staged, remove de volta os três campos (evidence, impact, verification) do fixture, reproduzindo exatamente a forma pré-correção. O achado volta a não ter evidência, o gate volta a descartá-lo, e tests/e2e/AUR-467.sh E2EAUR467 sai 1 (AUR-467/E2E/no-regression-gate-exit:0) de novo na cópia mutada — confirmando que a correção, e não outra coisa, é o que torna AC-001 verde.

Validação

  • go test -count=1 ./... — verde, sem alteração de comportamento de produto.
  • tests/e2e/AUR-467.sh E2EAUR467 (via go-shared exec, Go só em container) — exit 0 (era 1).
  • tests/acceptance/AUR-540.sh (via go-shared exec) — all, AC-001, AC-001-MUT-001 exit 0; seletor desconhecido (bogus) exit 64.
  • ./.board/bin/oci-run --profile bootstrap-readonly-v1 --card AUR-467 — exit 0. Este accept roda tests/acceptance/AUR-467.sh com o seletor padrão AC-001 (nominal_case), não E2EAUR467; não prova o seletor E2E dentro do container selado, só que a propriedade original do AUR-467 continua de pé. oci-run não aceita selecionar E2EAUR467 diretamente (só --profile/--card).
  • ./.board/bin/oci-run --profile bootstrap-readonly-v1 --card AUR-540 — exit 79 antes da correção do card, infraestrutura, ver "Gap medido" abaixo. Não era um RED comportamental.

Non-goals (herdados do card)

Nada em internal/prompt, internal/review ou na promessa do AUR-467 foi alterado. O gate de escopo e evidência permanece exatamente como estava.

Gap medido (resolvido): oci-run --card AUR-540 saía 79

./.board/bin/oci-run --profile bootstrap-readonly-v1 --card AUR-540 sai 79 com AUR-540/all/infrastructure/missing-input:tests/fixtures/repos/git-demo/repo.git. A materialização do oci-run só copia paths ∪ read_paths do frontmatter do card (.board/bin/oci-run, allowed_roots=("${owned_paths[@]}" "${read_paths[@]}")). Durante este card, o coordenador já corrigiu uma lacuna de read_paths (faltava internal/config, do qual cmd/aurumcode depende — confirmado com go list -deps ./cmd/aurumcode) e eu mergeei esse fix (main -> este worktree) antes de rodar de novo. Mas o oci-run ainda sai 79: tests/fixtures/repos/git-demo/repo.git -- exigido pelo próprio required_inputs de tests/e2e/AUR-467.sh -- continua fora de paths e read_paths do frontmatter de AUR-540. Isso está em .board/cards, fora dos paths que este card pode escrever e fora do que esta sessão foi instruída a tocar; precisa de outra correção de read_paths igual à que o coordenador já fez para internal/config (adicionar tests/fixtures/repos/git-demo).

./.board/bin/oci-run --profile bootstrap-readonly-v1 --card AUR-467 sai 0, mas isso NÃO prova o seletor E2E dentro do container selado: o accept daquele card roda tests/acceptance/AUR-467.sh sem argumento, que por sua vez usa o seletor padrão AC-001 (nominal_case), não E2EAUR467; e oci-run não aceita escolher outro seletor (só --profile/--card). Então nenhum dos dois oci-run hoje exercita E2EAUR467 dentro do container selado com os limites reais do perfil (256 MB, 120 s, 1 CPU, rootfs somente leitura, uid 65534).

Com isso, o AC-001 do AUR-540 está provado fora do oci-run — rodando tests/e2e/AUR-467.sh E2EAUR467 e tests/acceptance/AUR-540.sh all com Go só em container (go-shared exec, nunca no host), nunca no host — mas o accept declarado no card (oci-run --card AUR-540) continua vermelho por infraestrutura. Isso é um bloqueador para done, não um gap aceito: falta uma segunda correção de read_paths que esta sessão não pode aplicar.

Resolução (integração): o card passou a declarar tests/fixtures/repos/git-demo em read_paths. Com isso, ./.board/bin/oci-run --profile bootstrap-readonly-v1 --card AUR-540 sai com 0 (AUR-540/all/pass) no SHA integrado ao main, e o aceite do AUR-467 continua com 0.