Pular para conteúdo

AUR-424 — Extrator de documentação Go sem dependência externa

Documento histórico do board. Pode descrever componentes removidos; para uso atual, consulte o guia do produto.

Comando

AURUMCODE_SOURCE_DIR=<diretório do projeto Go> \
AURUMCODE_OUTPUT_DIR=<diretório de saída> \
go run ./cmd/regenerate-docs

Internamente, o comando acima delega a extração de páginas Go para internal/documentation/extractors/go.GoExtractor, através da chamada de registro já existente em cmd/regenerate-docs/main.go (registerLanguageExtractors):

if err := register(goExtractor.NewGoExtractor(runner)); err != nil {
    return err
}

Este card reescreve inteiramente internal/documentation/extractors/go; a assinatura NewGoExtractor(runner) é preservada de propósito para não exigir nenhuma mudança nesse call site.

O que mudou

Antes deste card, GoExtractor gerava documentação chamando o binário externo gomarkdoc via site.CommandRunner (runner.Run(ctx, "gomarkdoc", ...)). gomarkdoc não existe no sandbox offline (rede negada, sem instalação). Uma falha de exec.LookPath virava extractors.ToolUnavailableError, que o pipeline trata como "pular esta linguagem" — não como falha. Um projeto Go inteiro saía com zero páginas e exit 0, indistinguível de um projeto sem nada para documentar.

A partir deste card, GoExtractor usa somente a standard library:

  • go/build.Context.MatchFile seleciona quais arquivos .go do diretório pertencem ao pacote documentado (arquivos *_test.go são ignorados);
  • go/parser.ParseFile lê cada arquivo selecionado;
  • go/doc.NewFromFiles associa comentários de pacote, tipo, função e método às suas declarações — exatamente como go doc/pkg.go.dev fazem;
  • go/printer.Fprint renderiza a assinatura de cada declaração (sem corpo de função, sem comentário duplicado).

Alvo de build fixado (determinismo)

O filtro de build constraints não é um detalhe: sem ele, um diretório com arquivos mutuamente exclusivos de plataforma (impl_linux.go e impl_windows.go, ou dois arquivos com linhas //go:build opostas) tem todos eles reunidos em um único pacote, e a página resultante anuncia uma superfície de API que nenhum build daquele pacote jamais contém. Isso reprova diretamente a cláusula do AC-001 "repetir a execução sobre a mesma entrada produz a mesma saída" no sentido que importa: a saída deixa de descrever uma configuração real.

Por isso documentBuildContext() fixa o alvo em constantes, e não em runtime.GOOS/runtime.GOARCH:

Campo Valor Razão
GOOS linux Alvo fixo: a página não pode depender da máquina que a gerou.
GOARCH amd64 idem.
CgoEnabled false Caso contrário CGO_ENABLED do host decidiria o conteúdo.
BuildTags vazio Caso contrário -tags do host decidiria o conteúdo.

Consequência declarada: um arquivo *_windows.go, *_darwin.go ou guardado por //go:build windows não é documentado. Um diretório cujos únicos arquivos Go são excluídos pelo alvo fixado não é um pacote para efeito deste extrator, e não gera página nem erro.

Nome de arquivo de saída injetivo

O caminho do pacote (relativo à raiz do fonte) é achatado em um único nome de arquivo por outputBaseName. O esquema anterior trocava todo separador por _ e parava aí, o que não é injetivo: os diretórios a/b e a_b viravam ambos a_b.md, e o pacote escrito por último apagava a página do primeiro sem erro algum em ExtractResult.

O esquema atual reserva _ como introdutor de escape, de modo que todo _ na saída é o primeiro byte de uma sequência de dois:

Sequência Significado
_u um _ literal dentro de um segmento de caminho
__ um separador de caminho

Como u ≠ _, a decodificação é determinística da esquerda para a direita, logo a codificação é injetiva. Nomes sem separador e sem _ — o caso comum, ledger — passam intactos. O pacote na raiz do fonte não tem segmento algum e recebe o nome root; o único caminho real que colidiria com ele (um diretório de topo chamado root) é deslocado para root__, um segmento final vazio que filepath.Rel nunca produz.

Nenhum processo externo é iniciado; nenhuma chamada de rede é feita. GoExtractor.Validate agora sempre retorna nil: não há mais nada externo para verificar.

O tipo do parâmetro runner deixou de ser site.CommandRunner e passou a ser uma interface local mínima (commandRunner, com o mesmo único método Run). A satisfação de interface em Go é estrutural, então o site.CommandRunner que main.go já passa continua sendo aceito sem nenhuma mudança nesse arquivo; o pacote internal/documentation/extractors/go deixou de importar internal/documentation/site por completo, porque não tem mais nada para mandar um runner executar.

Opções (via variáveis de ambiente lidas por cmd/regenerate-docs)

Variável Efeito
AURUMCODE_SOURCE_DIR Raiz do projeto a documentar.
AURUMCODE_OUTPUT_DIR Diretório onde as páginas Markdown são escritas.
AURUMCODE_INCREMENTAL Quando true, pula pacotes cujo .md já é mais novo que o fonte.

GoExtractor.WithIncrementalMode expõe o mesmo controle programaticamente.

Exit codes

Estes são os exit codes do próprio GoExtractor.Extract (biblioteca) e do aceite deste card (tests/acceptance/AUR-424.sh), não os do binário completo cmd/regenerate-docs (que soma o resultado de todas as linguagens):

  • Extract retorna error != nil apenas para uma entrada inválida (idioma errado, diretório de origem inexistente, diretório de saída não gravável) ou cancelamento de contexto; um pacote individual que falhar ao parsear é reportado em ExtractResult.Errors e não interrompe os demais.
  • tests/acceptance/AUR-424.sh: ver "Exit codes do aceite" mais abaixo.

Exemplo executável (sem segredo)

Com um projeto Go mínimo em ./tests/fixtures/docs/goproject (fixture deste card: um pacote com um tipo exportado, dois métodos/funções exportados e comentários de documentação reais):

$ AURUMCODE_SOURCE_DIR=./tests/fixtures/docs/goproject AURUMCODE_OUTPUT_DIR=./site \
    go run ./cmd/regenerate-docs
...
✓ Source: .../tests/fixtures/docs/goproject | Output: ./site | ...
📊 Generated 1 markdown file(s) in ./site:
   - site/go/root.md
aurumcode: result=ok docs=1 skipped=0 failed=0 languages_skipped=none ...

site/go/root.md contém o comentário de pacote, type Greeting struct { ... } com seus campos e métodos (NewGreeting, String), e func Add/func Max com seus comentários de doc — símbolos e prosa reais extraídos do fonte, não um placeholder.

Nenhuma variável de ambiente com segredo é lida por este caminho de código.

Diagnóstico de falha

  • skipped: go: ... / zero páginas Go com exit 0 (comportamento pré-correção): sintoma exato do defeito que este card fecha — dependência de gomarkdoc ausente do sandbox. Reproduzido durante o TDD deste card restaurando temporariamente a implementação antiga (ver ## Prova).
  • no documentable Go package found in <dir>: o diretório tem apenas arquivos _test.go, ou nenhum arquivo .go, ou todos os pacotes encontrados são pacotes de teste externo (<pkg>_test).
  • Um símbolo existe no fonte mas não aparece na página: o arquivo que o declara é excluído pelo alvo de build fixado (linux/amd64, sem cgo, sem tags) — ver "Alvo de build fixado" acima.
  • Página gerada vazia ou ausente: extractors.ConfirmOutputFile recusa contar como gerado um arquivo que não existe, é diretório, ou tem zero bytes — o mesmo padrão de todo extrator deste diretório.

Nota de contrato: go run ./cmd/regenerate-docs dentro do sandbox de aceite

Na integração deste card, dois gaps medidos no candidato original foram reparados antes da revisão, e o card está done com essa correção incorporada:

  1. paths/read_paths foram ampliados para cobrir o fecho transitivo real de cmd/regenerate-docs/main.go — internal/pipeline, internal/llm e seus provedores, os extratores irmãos, internal/qa/browserproof (importado por tests/e2e/browser_chain_test.go), action.yml e .aurumcode, além dos cinco arquivos da tabela "Contratos pré-existentes" acima.
  2. A lane de regressão (internal/documentation/..., internal/pipeline/..., cmd/..., tests/e2e/..., tests/characterization/...) passou a rodar de fato dentro do accept selado, em vez de ficar fora do alcance do próprio aceite.

Hoje, dentro do container bootstrap-readonly-v1 (rede negada, apenas os arquivos allowlisted), go run ./cmd/regenerate-docs compila, e a lane E2E de tests/acceptance/AUR-424.sh executa o comando declarado de verdade — não mais um substituto — contra a fixture real deste card, com a lane de regressão incluída. O accept selado sai 0.

As lanes Unit e Integration continuam provando o comportamento deste card através do mesmo mecanismo de registro que main.go usa (extractors.Registry real, com o GoExtractor real, resolvido por linguagem), contra a fixture real deste card.

Fora do sandbox de aceite (checkout completo, com toda a árvore do módulo presente), o exemplo acima roda o mesmo binário completo e o aceite também sai 0 — o mesmo resultado dentro e fora do sandbox, agora que o gap de paths/read_paths foi fechado.

Escopo desta correção: somente Go

Este card fecha a dependência de ferramenta externa do extrator Go e de mais nenhum. Os outros sete extratores deste diretório — python (pydoc-markdown), javascript (typedoc), csharp (dotnet + xmldocmd), cpp (doxygen), rust (cargo), bash e powershell (pwsh) — continuam invocando binários externos e continuam sujeitos ao mesmo skip silencioso quando esses binários faltam. Nada aqui deve ser lido como "a classe de defeito foi fechada": ela foi fechada para uma linguagem.

Isso está refletido nos testes: as tabelas compartilhadas de tool_failure_test.go e tool_unavailable_test.go perderam apenas a linha do Go e mantêm intactas as asserções das outras sete linguagens.

Contratos pré-existentes que este card mudou de propósito

A correção muda um contrato que testes fora de paths deste card afirmavam. Cada um foi atualizado explicitamente, e não afrouxado:

Arquivo Antes Agora
internal/documentation/extractors/tool_unavailable_test.go linha go/gomarkdoc exigia ToolUnavailableError linha removida; TestGoExtractorValidate_HasNoExternalTool exige que nenhuma falha de runner torne o Go indisponível
internal/documentation/extractors/tool_failure_test.go linha go/gomarkdoc em realToolCases() linha removida; TestGoExtractorValidate_RealPATHIsIrrelevant exige sucesso com PATH vazio e com gomarkdoc quebrado no PATH
internal/documentation/extractors/output_confirmed_test.go linha go com toolWritesOutput: true toolWritesOutput: false — o Go passou a escrever a própria saída; a invariante "nada reportado sem prova em disco" continua rodando nessa linha
tests/characterization/legacy/documentation/characterization.go + classification.tsv probeGo fixava "chama o runner exatamente 1 vez" recaracterizado: zero chamadas externas e página confirmada em disco com o símbolo real
tests/e2e/smoke_test.go TestSmokeMissingToolchainFailsLoudly usava Go como linguagem sem ferramenta a fixture da caso troca Go por Python (que ainda depende de pydoc-markdown), preservando o piso "sem ferramenta nenhuma, o run falha alto"; TestSmokeGoDocumentsWithoutAnyToolOnPATH foi acrescentado para o fato novo

O piso de degradação não foi removido: ele foi mantido em uma linguagem que ainda o exerce.

Pendências nomeadas (não implementadas por este card)

  1. Os cabeçalhos gerados (## Types, ## Functions, ## Constants, ## Variables) não constam do structuralHeadings do scaffold do site.
  2. go/printer.Fprint emite tabulações; a saída não passa por gofmt, então os blocos de código podem carregar tabs em vez da formatação canônica.
  3. A guarda no-test-files de tests/acceptance/AUR-424.sh usa grep -Fq '\[no test files\]', cujo padrão literal (com as barras invertidas) nunca casa — a guarda está morta.
  4. selectPackage descarta em silêncio um segundo pacote não-teste no mesmo diretório, em vez de reportar a ambiguidade.
  5. O context.Context não é propagado para dentro de extractPackage: o cancelamento só é observado entre pacotes, não durante o parse de um.
  6. hasGoFiles passou a propagar o erro de matchedGoFiles, mas o filepath.Walk de findGoPackages descarta erros (return nil // Skip errors): um arquivo ilegível faz o diretório inteiro desaparecer em silêncio — mais uma instância da classe de skip silencioso que este card fecha para o caso do gomarkdoc, ainda aberta para o caso de I/O.

Limitações de infraestrutura medidas

./internal/... inteiro não é utilizável como lane de regressão

go test ./internal/... falha com internal/evidence [setup failed]: o teste desse pacote importa github.com/Mpaape/AurumCode/tests/integration, que contém arquivos package main (tests/integration/AUR-001.go, AUR-002.go, AUR-004.go) misturados com arquivos package integration, então o pacote não compila como importável. Isso é pré-existente e alheio ao AUR-424: reproduz-se identicamente em ec0495c, o commit pai deste candidato. Por isso a lane de regressão do aceite usa uma lista explícita de pacotes — ./internal/documentation/..., ./internal/pipeline/..., ./cmd/..., ./tests/e2e/... e ./tests/characterization/... — que é onde vivem todos os contratos que este card mudou (incluindo a classificação de skip em internal/pipeline, cujo significado mudou para o Go). tests/unit e tests/integration ficam de fora pelo mesmo motivo que internal/evidence: misturam package main com o pacote do diretório e não compilam como um todo; os seletores próprios do card já os cobrem via as lanes Unit e Integration. Fechar internal/evidence é trabalho de outro card.

Gap de paths/read_paths reparado na integração

Os cinco arquivos da tabela "Contratos pré-existentes" acima, que estavam fora de paths no candidato original, e o fecho de imports que a lane de regressão e o E2E real precisam (internal/pipeline, internal/llm, internal/qa/browserproof, action.yml, .aurumcode) foram incorporados a paths/read_paths pelo coordenador antes da revisão — ver "Nota de contrato" acima.

Com o gap fechado, tests/acceptance/AUR-424.sh sai 0 dentro de bootstrap-readonly-v1, com a lane de regressão executada de verdade e o E2E rodando o comando declarado real. A garantia que este card fecha permanece válida: um aceite verde continua exigindo que a lane realmente rode — ela roda, e não apenas está presente por acaso. Em checkout completo (árvore inteira presente), o mesmo script também sai 0, o mesmo resultado dentro e fora do sandbox.

Exit codes do aceite

  • 0 prova, com a lane de regressão executada;
  • 1 comportamento ausente (AUR-424/AC-001/behavior-missing), regressão fora do diff (AUR-424/AC-001/regression:<rc>), saída não determinística (AUR-424/AC-001/nondeterministic-output:...) ou dependência externa reintroduzida (AUR-424/AC-001/MUT-001/...);
  • 64 seletor desconhecido;
  • 69 infraestrutura (go ausente, module cache ausente, árvores não materializadas).

Prova

  • Unit tests/unit/AUR-424.go::TestAUR424: fixture em memória, zero chamadas ao runner, página gerada com símbolo e comentário reais, Validate sempre nil.
  • Integration tests/integration/AUR-424.go::IntegrationAUR424: registro real em extractors.Registry, extração da fixture rastreada tests/fixtures/docs/goproject duas vezes com digest idêntico (determinismo), e verificação textual do call site em cmd/regenerate-docs/main.go.
  • E2E tests/e2e/AUR-424.sh::E2EAUR424: execução real, não mais checagem estática de bytes. Roda o comando declarado (go run ./cmd/regenerate-docs) duas vezes sobre a fixture e compara os digests (cláusula de repetição do AC-001), verifica que a página gerada carrega os símbolos e comentários reais da fixture, e roda o binário compilado uma terceira vez com PATH vazio — o falsificador comportamental do MUT-001: um extrator que volte a chamar gomarkdoc não acha nada para chamar, reporta a linguagem como pulada e não escreve página.
  • Pacote internal/documentation/extractors/go: TestGoExtractor_BuildConstraintsPickOneTarget (a página traz o símbolo do alvo fixado e nenhum símbolo de outra plataforma, e duas execuções coincidem), TestGoExtractor_PlatformOnlyDirectoryIsNotAPackage, TestOutputBaseName_IsInjective e TestGoExtractor_CollidingPackageNamesBothSurvive.
  • Acceptance tests/acceptance/AUR-424.sh::AC-001 via ./.board/bin/oci-run --profile bootstrap-readonly-v1 --card AUR-424, agora incluindo a lane de regressão descrita acima.

Todo comando Go destes scripts roda com teto de memória embutido no próprio script (ulimit -v 8388608, GOMEMLIMIT=2GiB, go test -timeout 300s), para que um revisor que rode bash tests/acceptance/AUR-424.sh diretamente tenha a mesma proteção.