Pular para conteúdo

AUR-593: ergonomia medida pelo Aurum no Aurum

Medido antes (base f9a4f751)

Ao ligar o Aurum no próprio repositório (AUR-591) com a configuração mínima:

  1. Cobertura parcial por ignore e binário. mergeReviewCoverage (cmd/aurumcode/review_coverage.go) somava os caminhos do ignore e todo arquivo filtrado (binário, gerado, grande demais) em uncovered(); partial() ficava verdadeiro e, com inconclusive: block, a revisão saía partial_coverage. applyStructuralCoverage ainda retinha a aprovação por qualquer binário. No --pr, um binário sem patch entrava no prompt e era contado também como "omitido pelo limite de tokens".
  2. Semgrep na árvore inteira. Medido na imagem do produto (semgrep 1.172.0, p/security-audit + p/owasp-top-ten) sobre a árvore real: 23 resultados, a maioria em arquivos que nenhuma PR de card toca (Dockerfiles, demo/bad-code.go, dependabot.yml), e 13 erros nível warn (5 Syntax error, 8 PartialParsing). Causa do sast_invalid_output: (a) o campo type de um PartialParsing é um array (["PartialParsing", [...]]) e o adaptador o decodificava como string, então o json.Unmarshal do relatório inteiro falhava ("invalid JSON output"); (b) mesmo decodificado, qualquer entrada em errors tornava o relatório inválido, inclusive um parse recuperado num arquivo que a PR não tocou.
  3. Skill de diretório não citável. Só review.context.skills gerava regras (dynamicRulesFromLocalSkills/RemoteSkills); o catálogo de .aurumcode/skills/<nome>/SKILL.md só mandava texto ao prompt. Listar o arquivo nas duas fontes duplicava o texto, e o id era SKILL#<slug> para toda skill de diretório (colisão entre skills).
  4. govet sem go. A imagem final (alpine) não tinha o toolchain: lint_unavailable em toda revisão.

O que mudou

  • Cobertura (cmd/aurumcode/review_coverage.go, passes.go, structural_coverage.go, review_pr*.go, internal/analyzer/textdiff.go, internal/i18n/catalog.yml): caminho do ignore e binário (analyzer.NoticeReasonBinary) viram IgnoredPaths, fora de partial(); um arquivo conta uma vez (ignorado vence binário). O aviso de cobertura lista os ignorados mesmo com a revisão completa. Gerado, grande demais, sem patch e orçamento continuam parciais e retêm a aprovação. No --pr, declareBinaries tira o binário do diff antes do prompt (como o --base).
  • Escopo de mudança (internal/scanner/added_lines.go, movido de internal/scanner/govet/diff.go, um lugar só): scanner.AddedLines, LineSet.Keep/Touched, scanner.ErrNoRange. O govet usa o mesmo código. O semgrep (internal/scanner/semgrep) continua varrendo a árvore, guarda só achados em linhas adicionadas, decodifica type cru (json.RawMessage) e só trata como inválido o erro que alcança a mudança: nível diferente de warn, erro sem arquivo, warn em arquivo tocado sem trecho, ou trecho sobre linha adicionada. Sem intervalo revisado: inconclusivo. O semgrep recebe só o safe.directory do ambiente (o git diff roda num checkout de outro usuário no CI).
  • Skills (internal/context/skills/catalog.go, internal/review/skillrules.go, cmd/aurumcode/policygate.go, skill_catalog.go, review_base_analysis.go, review_pr_model.go): o catálogo guarda a camada de cada skill (LayerPolicy/LayerRepository) e expõe Docs(changed); dynamicRulesFromCatalog transforma cada skill selecionada em regras com a origem da sua camada (uma skill do repositório nunca ganha origem de política). ExcludeListed tira do catálogo o SKILL.md que a mesma camada lista em context.skills (texto uma vez). O id de um SKILL.md é <diretório>#<slug> nas duas fontes. No --base o catálogo é montado antes das regras, e as regras entram no digest do catálogo de regras (cache).
  • Imagem (Dockerfile): COPY --from=builder /usr/local/go da imagem golang:1.27.1-alpine3.24@sha256:8a59... (a mesma do build, a do go.mod), PATH, GOPATH=/go, GOMODCACHE=/go/pkg/mod, e o build falha se go version não for go1.27.1. Workflow (review.yml): passo "Prefetch Go modules for the lint engine" (go mod download com GOTOOLCHAIN=local, sem segredos, só com go.mod), cache montado em /go/pkg/mod na review; download que falha é aviso e o govet ligado fica lint_execution_error (fail-closed).
  • Tutoriais: revisao, gate, reaproveitamento e auditoria-sarif usavam um PNG para forçar cobertura parcial; agora usam um arquivo gerado (repo-exemplo/gerado/gerado.go). revisao/falha-nao-revisado ganhou uma terceira parte (terceira parte do caso: PNG + inconclusive: block → exit 0, logo.png (binary) listado). sast/govet-sem-go tira o Go do PATH (a imagem agora o traz). Todos os out/ regravados com a imagem deste worktree; capturas regravadas.
  • Docs: configuration.md (ignore/binário fora da cobertura, escopo do semgrep e dos erros de parse, id de skill de diretório, Go na imagem e prefetch), getting-started.md (seção "Configuração mínima"), architecture.md, tutoriais revisao, gate, qualquer-linguagem, sast.

Prova

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

$ bash tests/acceptance/AUR-593.sh all
AUR-593/AC-001/pass
AUR-593/AC-002/pass
AUR-593/AC-003/pass
AUR-593/AC-004/pass
AUR-593/AC-005/pass
    aur593_test.go:49: exit=1, want 0 and no partial_coverage: an ignored path and a binary are declared, not partial
--- FAIL: TestIgnoredAndBinaryAreDeclaredNotPartial (0.11s)
AUR-593/MUT-001/rejected
    aur593_test.go:111: exit=3, want 0
--- FAIL: TestSemgrepJudgesOnlyTheChange (0.11s)
AUR-593/MUT-002/rejected
AUR-593/all/pass
  • AC-001: TestIgnoredAndBinaryAreDeclaredNotPartial (tests/** ignorado + PNG, inconclusive: block → exit 0, sem partial_coverage, "2 file(s) ignored" com os dois nomes); TestGeneratedFileStillPartialUnderBlock (fail-closed); TestAUR522PRBinaryIsDeclaredIgnored e TestAUR522PRGeneratedFileIsNotReviewed (--pr).
  • AC-002: TestSemgrepJudgesOnlyTheChange (achado pré-existente em arquivo não tocado + erro de parse fora da mudança → exit 0; achado numa linha adicionada → exit 3) e os testes da engine (change_scope_test.go: array em type decodifica; erro sobre linha adicionada, arquivo tocado sem trecho, erro sem arquivo e erro fatal continuam ErrInvalidOutput; sem intervalo, ErrNoRange).
  • AC-003: TestDirectorySkillIsCitableOnce: só diretório e diretório + listado; o gate reprova (exit 3) citando tamanho#tam-001-funcao-com-no-maximo-150-linhas e o marcador do texto aparece 1 vez no prompt capturado.
  • MUT-001: partial() volta a contar Ignored → AC-001 vermelho (exit 1).
  • MUT-002: semgrep devolve findings sem Keep → AC-002 vermelho (exit 3 no caso pré-existente).

AC-004 na imagem do produto (Go real, sem fake)

docker build -t aurumcode-review:aur593 . deste worktree; repositório de demo/tutoriais/sast/repo-exemplo/lint-base + lint-erro com dois commits:

$ docker run ... aurumcode-review:aur593 review --base main
calc/calc.go:9: [warning] fmt.Printf format %d has arg s of wrong type string (rule go-vet/printf)
aurumcode review: policy gate: go-vet/printf - ... (severidade warning, limiar warning, origem govet, secao repo)
rc=3

AC-005 na imagem do produto, sobre intervalos reais deste repositório

Clone do repositório; .aurumcode/ do card/AUR-591 com {engine: semgrep, enabled: true, required: true} e {engine: govet, enabled: true, required: true, fail_on: warning} (com deliberation ligada, um scanner não required viraria ferramenta do modelo e o provedor de fixture nunca o chamaria). Modelo: fixture de aprovação limpa. Cache de módulos do projeto montado só leitura em /go/pkg/mod (o que o passo de prefetch faz no CI). Wrappers no PATH registram cada execução:

$ docker run ... review --base main --auditoria /out/audit.json   # card/AUR-591 (intervalo f9a4f751..c163d06a)
rc=0
gitleaks git --log-opts=f9a4f751...c163d06a --report-format json ...
semgrep scan --json --quiet --metrics=off --disable-version-check --config p/security-audit --config p/owasp-top-ten .
go env GOVERSION
go vet -json ./...
audit: "gate": {"decision": "pass"}, "coverage": {"complete": true, "omitted_files": [40 itens: tests/acceptance/*.sh, docs/assets/capturas/*.png (binary), ...]}
Cobertura da revisão
- 40 arquivo(s) ignorados (configuração `ignore` do repositório ou binários): ...

Sobre o intervalo deste card na cabeça final (main...71fde6d6, 109 arquivos, mesma configuração): os três scanners rodaram e nenhum ficou inconclusivo (semgrep e govet sem achado nem erro). O resultado foi rc 3, por dois motivos que não são deste card e ficam medidos:

  • gitleaks:sumologic-access-token (87 achados) em docs/assets/capturas/capturas.json, nos commits que regravam as capturas: os digests sha256 do manifesto casam com a regra. O gitleaks lê o histórico e o ignore não o filtra. Toda PR que regrava capturas terá o mesmo achado: falso positivo a tratar (exceção com dono/validade ou allowlist), fora deste card.
  • partial_coverage pelo limite de tokens (7 arquivos de 109 fora do prompt) e pelos 4 gerado.go dos tutoriais (gerados de propósito): parcial legítimo, não por ignore nem binário (55 ignorados/binários listados à parte).

v3 (gitleaks limpo). O gerador scripts/docs/capturas.cjs grava "insumo_sha256":"sha256:<hex>": o hex já não segue direto o separador e a regra não casa; scripts/docs/capturas-check.sh exige o prefixo (valor nu reprova: entrada invalida no manifesto, medido) e o remove antes de comparar. Como o gitleaks lê cada commit do intervalo, os commits antigos da branch com o manifesto nu ainda acusavam; a branch do card (nunca publicada) foi achatada num commit sobre o main. Nova revisão do intervalo 996732f0..8c7fcdc8 (111 arquivos), mesma configuração, imagem reconstruída: gitleaks, semgrep e go vet executados, nenhum achado de gitleaks e nenhum scanner inconclusivo; rc 1 só por partial_coverage do limite de tokens (6 arquivos fora do prompt), o limite conhecido registrado abaixo. AUR-588.sh all e AUR-593.sh all: all/pass.

A seção "Configuração mínima" de getting-started.md, copiada literalmente para um repositório de um módulo Go, passa no Parse estrito e sai rc 0 na imagem (com rede; sem rede o pacote p/... não baixa e o semgrep fica sast_execution_error, fail-closed).

Regressão

  • go vet ./... && go test -p 1 ./... (go-shared): verde.
  • Tutoriais: os 18 run.sh all em paralelo com aurum-tutoriais:aur593 (todos rc 0) e --check de cada um: verde.
  • bash scripts/docs/capturas.sh e bash scripts/docs/build.sh: 0 warnings.
  • Aceites (go-shared): AUR-575, 577, 581, 565, 579, 584, 562, 563, 588 all/pass (ou a última linha ok). Falham também no main sem este card (medido no checkout principal): AUR-590 AC-003/go-list-offline-failed e AUR-564 (corrigido abaixo). AUR-561 voltou a passar depois de o caso do binário virar parte de falha-nao-revisado (caso novo exigiria registro em docs/specs/AUR-561.md, fora de paths). Na v2 do card (AUR-492.sh e AUR-564.sh em paths): o MUT-002 do AUR-492 muta agora internal/scanner/added_lines.go (mesma âncora if s[f.Path][f.Line] {, para onde este card moveu o mapeamento do diff) e AUR-492.sh all termina AUR-492/all/pass; o bloco <!-- arquivo: .board/oci/profiles/go-unit-offline-v1.json --> de docs/tutorials/operacao.md não acompanhou o repin do AUR-590 (lock_digest 979c3003... no texto, 026e9310... no arquivo); alinhado, AUR-564.sh all sai rc 0 (AC-004/ok (14 blocos identicos aos arquivos da demo...)). AUR-590 AC-003/go-list-offline-failed falha igual no main.

Revisão: byte NUL não esconde código

O revisor provou falha aberta: um único byte NUL nos primeiros 8000 bytes (grammar.LooksBinary) fazia um .sh com curl … | sh virar "binário ignorado", e a revisão aprovava sob inconclusive: block sem o modelo ler o arquivo. Conserto: só é declarado ignorado o arquivo cuja extensão está no catálogo internal/grammar/catalog/binary_formats.yml (imagens, PDF, fontes, mídia, zip; executáveis e bibliotecas ficam de fora de propósito) e cujo conteúdo é binário (grammar.KnownBinaryFormat + declaredBinary em cmd/aurumcode/structural_coverage.go, um lugar para --base e --pr). Script, código, config, arquivo sem extensão (shebang), extensão desconhecida ou catálogo ilegível: parcial e aprovação retida. Testes: TestNULInCodeIsNeverDeclaredBinary (deploy.sh, app.js, ci.yml, instala sem extensão, cada um com NUL, sob block → exit 1, partial_coverage, sem Approve), TestAUR522PRBinaryWithoutPatchIsNotReviewed (tool.bin no --pr, retido) e TestAUR522PRBinaryIsDeclaredIgnored (logo.png real, ignorado). MUT-003 (qualquer conteúdo binário declarado, sem o catálogo) deixa os três casos com extensão vermelhos (exit=0, want 1):

AUR-593/MUT-003/rejected
AUR-593/all/pass

Segundo fechamento: o revisor provou lib/payload.PNG (script + NUL) declarado ignorado. O catálogo passou a exigir, por formato, a assinatura (magic bytes em hex e offset: PNG 89504E470D0A1A0A, JPEG FFD8FF, GIF 474946383761/…3961, PDF 25504446, ZIP 504B0304/504B0506, WOFF 774F4646, WOFF2 774F4632, MP4 66747970 no offset 4, …); formato sem assinatura é erro de carga. grammar.DeclaredBinaryFormat(nome, conteúdo) exige extensão (qualquer caixa) + assinatura + conteúdo binário; o analisador grava o resultado em DiffNotice.DeclaredFormat (lado novo, ou o antigo de um arquivo apagado). Testes: lib/payload.png e lib/payload.PNG com script e NUL → exit 1, retidos; docs/Captura.PNG real → ignorado. MUT-004 (assinatura não conferida) deixa os dois payload vermelhos (exit=0, want 1); AUR-593/all/pass.

O tutorial qualquer-linguagem/binario-e-gerado voltou a reter ferramenta.bin e leia-me.txt (fora do catálogo); tutoriais e capturas regravados.

Decisões e pendências

  • Limite conhecido: orçamento de tokens numa PR grande. Na revisão do intervalo deste card (109 arquivos), 7 arquivos ficaram fora do prompt pelo limite de tokens e a revisão saiu partial_coverage. É parcial legítimo (o modelo não leu esses arquivos) e não muda neste card: uma PR grande continua inconclusiva sob inconclusive: block até um card tratar o orçamento (ex.: revisão em lotes).

  • Um arquivo de formato binário do catálogo não retém mais a aprovação (antes AUR-522): nada nele se revisa lendo; ele é listado pelo nome. O segredo num binário continua com o gitleaks (intervalo de commits). Gerado, grande demais e sem patch seguem retendo.

  • Erro de parse warn do Semgrep num arquivo tocado, sem trecho indicado (Syntax error de arquivo inteiro, ex.: alguns run.sh deste repo), segue inválido: fail-closed sobre código que a PR adicionou e o Semgrep não leu. Uma PR que mexe num desses scripts com o semgrep ligado fica sast_invalid_output; o repositório pode tirar o caminho com .semgrepignore (sem política) — não medido aqui sobre um PR real.
  • O ignore do repositório não filtra os scanners (só o que o modelo lê); achados de scanner em caminho ignorado continuam contando se estiverem em linhas adicionadas.
  • Repositório Go com módulo privado: o prefetch falha (aviso) e o govet ligado fica lint_execution_error; credenciais de módulo privado ficam fora deste card.
  • .aurumcode/config.yml deste repositório não foi alterado (fora de paths); ligar semgrep/govet nele é do AUR-591.