Cache de revisão: identidade, evidência entre arquivos e comportamento de degradação (AUR-513)
Este documento estende o cache de revisão por arquivo do AUR-441 (cmd/aurumcode/review_cache.go,
internal/review/cache). Veja docs/specs/AUR-441.md para o desenho
original (uma entrada de cache por arquivo alterado, chaveada pelo conteúdo do
diff bruto desse próprio arquivo, mais a identidade do modelo e uma constante
de versão do prompt). Aqui está o que o AUR-513 acrescenta: toda outra entrada
que pode mudar a resposta do modelo, como um acerto por arquivo preserva a
evidência entre arquivos e como um cache quebrado degrada.
O que agora faz parte da chave do cache
reviewContextCacheKey (cmd/aurumcode/review_cache.go) incorpora, além dos
três itens originais do AUR-441 (idioma da revisão, pacote de contexto da
base de código, notas de memória):
- O modelo/endpoint que de fato responde, capturado antes de qualquer
encapsulamento de contexto (
baseModelIdentity):modelCacheKey(provider)chamado sobreproviderno instante em que ele é selecionado, ANTES deconfig.WrapProviderWithWarningster qualquer chance de encapsulá-lo. Isto não é redundante com a chamadamodelCacheKey(provider)quereviewContextCacheKeyainda faz sobre o provider final (a essa altura, possivelmente encapsulado e possivelmente encapsulado por--limite) — veja "Por que a identidade do modelo é capturada duas vezes" abaixo para o bug que isso fecha. - Perfil(is) de revisor selecionado(s) (AUR-502): a concatenação de
reviewprofile.Profile.Signature()de cada perfil selecionado, na ordem de seleção. A ordem importa porquereviewprofile.MergeFindingsatribui um achado compartilhado ao perfil mais antigo na ordem de declaração; portanto, reordenar o mesmo conjunto de nomes pode mudar a resposta. Antes deste card,partitionByCacheera chamada com uma chave calculada antes derunProfilePassessaber quais perfis foram escolhidos, de modo que duas revisões do mesmo diff sob perfis diferentes podiam, por engano, compartilhar uma única entrada de cache. - O conteúdo do bloco de contexto montado e REDIGIDO (prompt do repositório
- skills + docs e, sob uma política central, o da própria política —
AUR-518):
contextBlockCacheDigestfaz o hash do mesmo bloco queconfig.BuildContextBlockWithWarningsmonta para o prompt de saída real. Veja "Por que o digest do contexto faz hash do bloco redigido" abaixo. - O catálogo dinâmico de regras/seções de skill (AUR-519):
ruleCatalogCacheDigestfaz o hash da lista ordenada de IDs do catálogo de regras e do mapadynamicRulesordenado (oreview.Rulecompleto de cada entrada, não só o ID) — é isso que o modelo aprende e contra o que o gate aceita citações. Veja "ruleCatalogDigest: defesa em profundidade, não exercitado isoladamente de ponta a ponta" abaixo para o estado atual dos testes.
Duas revisões idênticas em todos esses itens podem reutilizar legitimamente uma entrada de cache; uma diferença em qualquer um deles deve produzir uma chave diferente.
Por que a identidade do modelo é capturada duas vezes
config.contextInjectingProvider (internal/config/wrap.go, o tipo que
WrapProviderWithWarnings devolve assim que QUALQUER contexto é configurado —
um prompt do repositório, uma skill, um doc) embute llm.Provider como um
campo de interface. Go só promove métodos declarados no tipo estático do
campo embutido; logo, um método que o provider concreto subjacente também
implementa, mas que não faz parte da própria interface llm.Provider, não é
promovido pelo wrapper. llm.ModelResolver (ResolveModel) é exatamente um
método assim: internal/llm/provider/litellm.Provider o implementa (reportando
o modelo realmente configurado), mas, uma vez encapsulado, uma asserção de tipo
provider.(llm.ModelResolver) sobre o valor encapsulado falha, e
modelCacheKey recorre silenciosamente ao literal fixo "litellm" de
litellm.Provider.Name() — sem modelo, sem endpoint.
Antes desta correção: com QUALQUER doc/skill/prompt configurado (comum), duas
revisões contra o mesmo endpoint com dois valores diferentes de LLM_MODEL
colidiam numa só entrada de cache, porque a única coisa que as distinguia (o
modelo reportado por ResolveModel) tinha se tornado invisível. O
fixedModelProvider de cmd/aurumcode/cost.go documenta esse mesmo risco de
perda de promoção para outro wrapper e o contorna implementando ResolveModel
diretamente em si mesmo — mas isso só corrige o wrapper próprio do --limite,
não o wrapper de contexto que está por baixo, e apenas quando --limite é
realmente usado.
A correção: runReview (cmd/aurumcode/main.go) chama modelCacheKey(provider)
imediatamente após a seleção do provider e antes de qualquer encapsulamento,
captura o resultado como baseModelIdentity e passa essa string a
reviewContextCacheKey incondicionalmente. A chamada preexistente
modelCacheKey(provider) dentro do próprio reviewContextCacheKey é mantida,
inalterada, porque o fixedModelProvider do --limite (encapsulado depois,
sobre o wrapper de contexto, e implementando ResolveModel diretamente — sem
depender de promoção) ainda precisa desse valor capturado tarde para a
contabilidade da chave de custo, exatamente como antes do AUR-513; removê-la
regrediria tests/acceptance/AUR-433.sh. Os dois valores são complementares,
não alternativas.
modelCacheKey também passa a incorporar LLM_BASE_URL diretamente (dois
endpoints que servem um modelo com o mesmo nome não são a mesma entidade
respondente). TestAUR513ModelIdentitySurvivesContextWrapping
(cmd/aurumcode/aur513_test.go) prova as duas metades: uma verificação
unitária de que dois litellm.Provider com modelos E URLs base diferentes,
encapsulados pelo mesmo contexto, ainda produzem chaves de cache de revisão
diferentes (e uma asserção de sanidade de que o modelCacheKey encapsulado
sozinho colide, demonstrando o bug que isso fecha); e uma execução de ponta a
ponta contra um httptest.Server real com contador de requisições, em que
trocar LLM_MODEL entre duas rodadas contra um cache persistido força uma
segunda requisição de verdade.
Por que o digest do contexto faz hash do bloco redigido
contextBlockCacheDigest faz o hash do bloco que
config.BuildContextBlockWithWarnings monta — o MESMO texto, já redigido, que
a chamada real envia ao modelo — em vez da saída bruta, pré-redação, de cada
provider.
Uma revisão anterior desta função fazia hash da saída bruta dos providers
justamente para evitar uma colisão: redaction.Filter.Redact substitui todo
trecho com formato de segredo pelo mesmo marcador fixo [REDACTED]; assim,
dois arquivos que diferem apenas no valor de um segredo podem ser redigidos
para um texto idêntico byte a byte, e um teste que mudava apenas um marcador
com formato de segredo entre rodadas provou exatamente isso — o digest não se
movia, porque o texto sobre o qual era calculado também não se movia, uma vez
que a identidade do modelo e a abordagem sobre texto bruto discordavam do que
o modelo realmente recebe.
Essa colisão é real, mas fazer hash do texto bruto para driblá-la responde a
uma pergunta diferente de "isto deve contar como acerto de cache": o modelo só
vê o prompt REDIGIDO. Se duas configurações produzem o bloco redigido
idêntico, o modelo responderia identicamente nos dois casos, e reutilizar a
resposta em cache está correto — não é uma fraqueza, é apenas aquilo que o
próprio modelo já não consegue distinguir. Fazer hash do texto bruto dos
providers tornaria o cache MAIS conservador que o modelo para o qual ele
guarda respostas, forçando re-revisões desnecessárias por uma diferença que o
modelo nunca vê, sem mudar em nada a história de exposição de segredos (o
digest, bruto ou redigido, é de qualquer forma uma string hex sha256 de mão
única — o padrão já estabelecido de cache.Key para o conteúdo do diff de um
arquivo —, então "nenhum segredo em forma legível" vale nas duas versões).
TestAUR513AC002DocContentChangeForcesFreshReview portanto muda uma linha
claramente visível (prosa comum, sem formato de segredo) entre as rodadas,
de modo que o teste realmente exercite "o texto configurado chega ao digest" e
não uma colisão de redação sob a qual ambas as versões se comportariam de
forma idêntica.
Isso ainda chama config.BuildContextBlockWithWarnings uma segunda vez em vez
de ler as entranhas do provider já encapsulado, porque
contextInjectingProvider não é exportado e não expõe nenhum ponto de
costura para um chamador fora de internal/config recuperar o texto do seu
bloco, e os paths deste card não incluem internal/config. Todo provider
que ConfiguredProviders devolve (RepoPromptProvider,
FileContextProvider, TextContextProvider, PathInstructionsProvider) lê
um arquivo local e não faz mais nada; para eles, uma segunda chamada é
determinística e livre de efeitos colaterais. As fontes MCP de
review.context.mcp também entram na lista de providers passada ao digest
(review_base_analysis.go e review_pr_model.go): montar o bloco só para o
digest consulta cada servidor MCP de novo, e uma resposta diferente do servidor
muda a chave de cache. Evitar essa segunda consulta exigiria memoizar o bloco
de uma mesma revisão para o digest e para o prompt.
ruleCatalogDigest: defesa em profundidade, não exercitado isoladamente de ponta a ponta
Hoje, todo arquivo de skill que dynamicRulesFromLocalSkills lê
(review.ParseSkillSections) vem exatamente dos mesmos arquivos que o bloco
já hasheado por contextBlockCacheDigest contém; assim, na prática, uma
mudança de conteúdo de skill que altera o catálogo de regras derivado também
altera o digest do bloco de contexto, e os testes deste card invalidam o
cache por esse caminho compartilhado, em vez de isolar este diretamente.
ruleCatalogDigest é mantido mesmo assim, incorporado de forma independente
à chave, porque os dois podem divergir sem aviso no futuro: uma entrada de
catálogo derivada com uma normalização que os bytes brutos do bloco não
refletem, uma fonte de regras exclusiva da política central, ou qualquer
contribuição de regra que não seja simplesmente "os bytes de um arquivo de
contexto" mudaria o que o modelo aprende e o que o gate aceita sem
necessariamente mudar o próprio bloco de contexto. Esta é uma lacuna
documentada, não uma alegação de prova de ponta a ponta para este componente
isoladamente.
AC-003: um acerto por arquivo não pode descartar evidência entre arquivos
Abordagem escolhida: apoiar-se no pacote de contexto da base de código já ser calculado a partir do diff completo, sem partição, antes de qualquer consulta de cache por arquivo, e já fazer parte da chave do cache.
Em runReview (cmd/aurumcode/main.go), resolveCodebaseContext(diff) roda
sobre o diff completo revisado — todo arquivo alterado, acerto ou falha — e só
depois partitionByCache reduz o diff enviado ao modelo apenas às falhas
(toSend). codebasectx.Resolver.Resolve (internal/context/resolver.go)
lê o conteúdo de cada caminho alterado a partir do checkout e extrai seus
símbolos definidos e as arestas de referência entre arquivos para
Pack.Symbols/Pack.References/Pack.Dependents, independentemente de quais
arquivos acabarão sendo acertos de cache. Esse pacote é serializado em
codebaseContextText, que é (a) embutido literalmente no prompt de saída (a
seção "## Codebase context" de internal/prompt/builder.go, via
review.ReviewContext.CodebaseContext) e (b) uma das entradas de
reviewContextCacheKey. Assim, os símbolos de um arquivo com acerto de cache
ainda chegam ao modelo que revisa um arquivo irmão alterado, e uma mudança
nesse quadro entre arquivos ainda invalida as entradas certas.
Concretamente: se lib.go define HelperZZZ e é revisado uma vez junto com
app.go (que o chama), então, numa rodada posterior em que só app.go muda
de novo e o diff de lib.go contra a base fixa é idêntico byte a byte ao da
rodada em cache (um acerto de cache genuíno, nunca reenviado), HelperZZZ
ainda aparece no pacote de contexto da base de código dessa rodada, porque o
pacote foi construído a partir dos caminhos dos dois arquivos antes de a
partição rodar. TestAUR513AC003CrossFileEvidenceSurvivesPartialHit
(cmd/aurumcode/aur513_test.go) prova isso de ponta a ponta, via
AURUMCODE_PROMPT_CAPTURE: ele analisa especificamente a seção JSON
## Codebase context do prompt capturado e afirma que HelperZZZ está no seu
campo Symbols — NÃO apenas uma checagem de substring contra o prompt
inteiro, porque o diff do próprio app.go contém literalmente o texto
HelperZZZ() no ponto de chamada, o que faria uma checagem de substring pura
passar mesmo com um pacote de contexto vazio. O seletor AC-003-MUT-001 de
tests/acceptance/AUR-513.sh muta a construção de reviewCtx para resolver o
contexto da base de código a partir de toSend (o diff já particionado, só
com falhas) em vez do diff completo, reproduzindo a regressão que este
desenho existe para recusar, e confirma que o teste fica VERMELHO.
Alternativa considerada e rejeitada: recusar servir um acerto de cache
para qualquer arquivo do qual o conjunto de arquivos recém-alterado possa
depender. Isso exige um grafo de dependências que o pacote de cache não tem, e
construir e manter um confiável (quais arquivos dependem estaticamente de
quais, transitivamente, entre linguagens) é um empreendimento muito maior do
que o Outcome deste card pede, e arrisca a falha oposta — tratar um arquivo
não relacionado como "dependido" e forçá-lo a ser reenviado para sempre,
frustrando silenciosamente todo o propósito do cache. O pacote de contexto da
base de código já dá ao modelo o quadro entre arquivos de que ele precisa sem
esse grafo, e já estava ligado antes deste card exatamente por essa razão
(AUR-515/AUR-536); o trabalho deste card foi confirmar, por teste, que
nada na partição do cache estreita o que esse pacote enxerga, e incorporá-lo
(junto com as demais novas entradas de identidade da revisão)
corretamente à chave.
AC-004: um cache quebrado degrada para uma revisão nova, nunca para uma aprovação
Dois pontos de falha independentes, ambos preexistentes no desenho do AUR-441 e inalterados por este card, exceto por serem aqui provados por teste:
- Uma entrada corrompida (
cache.Cache.Get,internal/review/cache/cache.go): um erro de leitura ou de parse de JSON devolve(nil, false, err).partitionByCache(cmd/aurumcode/review_cache.go) só trata uma consulta como acerto quandogetErr == nil && ok; qualquer outro resultado — inclusive um erro de parse numa entrada truncada/corrompida em disco — devolve esse arquivo à lista de falhas, de modo que ele é revisado do zero e seus achados reais aparecem normalmente.TestAUR513AC004CorruptCacheEntryDegradesToFreshReviewescreve uma entrada corrompida por cima de uma entrada real em cache e prova que a segunda rodada, sob o mesmoAURUMCODE_LLM_FIXTUREda rodada 1 (deliberadamente: mudar o fixture entre as rodadas também mudariamodelCacheKeye forçaria uma chave nova por si só, sem provar nada sobre o caminho da entrada corrompida em especial), nunca imprime uma nota de reutilização e de fato (re)escreve um arquivoAURUMCODE_PROMPT_CAPTUREnomeandoapp.go.AC-004-MUT-001(tests/acceptance/AUR-513.sh) mutapartitionByCachepara tratar qualquer erro deGetcomo um acerto com zero issues e confirma que este teste fica VERMELHO. - Um diretório de cache inutilizável (
cache.Open,internal/review/cache/cache.go): um diretório que não pode ser criado ou usado devolve um erro.runReviewtrata isso exatamente como "nenhum cache configurado para esta execução" —toSendcontinua sendo o diff completo, sem partição, epersistFreshResults/mergeCacheHitssão ignorados por completo (condicionados acacheErr == nil). O cache é uma otimização de melhor esforço, nunca um portão de corretude.TestAUR513AC004UnreadableCacheDirDegradesToFreshReviewapontaAURUMCODE_CACHE_DIRpara um caminho que já existe como arquivo comum (de modo queos.MkdirAllfalha) e prova que a revisão ainda roda por inteiro e ainda reporta o achado real.
Nos dois casos, vale a garantia relevante para o gate já estabelecida para AUR-441/AUR-519: uma falha de cache só pode custar uma chamada repetida ao modelo, nunca transformar silenciosamente um resultado inconclusivo/bloqueado em aprovação, e nunca omitir um arquivo da cobertura.
Lacuna fechada: o componente de versão do prompt na chave agora é um digest em tempo de execução (AUR-543)
Antes deste card, internal/review/cache.PromptVersion ("v1") era uma
alavanca incrementada à mão: no dia em que o prompt embutido ou o catálogo de
regras embutido mudasse de um jeito que pudesse alterar os achados de um
arquivo, um humano tinha de lembrar de incrementar esse literal manualmente, ou
toda entrada de cache construída sob o prompt antigo continuaria sendo servida
sob o novo.
internal/prompt.PromptBuilder.FixedContentDigest() fecha essa lacuna. Ele
chama o próprio BuildPrompt — o ponto de entrada público exato que as
chamadas de revisão em produção usam — DUAS vezes, uma para cada par fixo
sentinela de diff/opções (fixedContentSentinelPairs, builder.go:
fixedContentSentinelDiffCode com fixedContentSentinelOptsNonEmptyCI, e
fixedContentSentinelDiffDocsOnly com fixedContentSentinelOptsEmptyCI), com
métricas sentinela fixas, e então faz o hash da concatenação do texto completo
System+User das duas chamadas MAIS uma chamada direta a
renderCoverageDeclaration, de coverage.go, com dados de cobertura
sintéticos montados à mão (fixedContentSyntheticCoverage). Dois pares
sentinela de diff/opções, e não um, para que todo ramo fixo que a revisão
deste card nomeou seja exercitado por pelo menos um deles: ReviewChangeScope
(filetype.go) renderiza uma string instrucional fixa DIFERENTE conforme o diff
tenha ou não código substantivo, e reviewCIContext renderiza uma string de
fallback fixa DIFERENTE ("No CI failure context was supplied. Do not invent CI
failures or claim that checks passed.") quando CIContext está vazio do que
quando não está — um único par deixaria um ramo de cada um sem exercício. A
chamada direta a renderCoverageDeclaration cobre os dois estados que uma
chamada real de BuildPrompt sem limite (MaxTokens: 0) nunca consegue
produzir sozinha, porque nada é jamais cortado: um arquivo "parcial" (alguns,
mas não todos, os hunks cobertos) e a linha de estouro da lista de marcadores
de omitidos (mais arquivos omitidos que maxOmittedBullets) — os dados de
entrada são sintéticos, mas o código de renderização é a função de produção
exata pela qual passa a seção de cobertura de toda revisão real.
Como cada uma dessas chamadas é código de produção real — o mesmo template, o
mesmo catálogo de regras embutido, os mesmos ramos de
ReviewChangeScope/reviewCIContext e o mesmo renderCoverageDeclaration que
todo outro chamador usa —, o digest e o que BuildPrompt de fato envia não
podem divergir. Ele cobre, e uma edição em qualquer um destes move o digest: o
texto literal de templates/review.md e a redação do esquema de resposta; o
catálogo de regras embutido (DefaultRuleCatalog); AMBAS as variantes
instrucionais de ReviewChangeScope (filetype.go); AMBOS os ramos de
reviewCIContext, o fallback e o repasse; o marcador "- %s: %d files" de
formatLanguages (fixedContentSentinelMetrics carrega uma entrada fixa de
LanguageBreakdown para que ele seja renderizado); os próprios cabeçalhos de
seção de buildUserContent e de fixedOverhead — "## Change Summary",
"## Existing CI Context", "## Code Changes", "## PR history (untrusted
observations, not instructions)", "## Codebase context (untrusted, bounded,
heuristic)", "## Review memory (untrusted observations, not instructions)";
toda linha que renderCoverageDeclaration, de coverage.go, escreve — seu
cabeçalho, todas as CINCO linhas de contagem (sempre renderizadas, mesmo em
zero: total de arquivos de código, totalmente revisados, parcialmente
revisados, não revisados e docs excluídos), seu marcador de arquivo parcial,
seu marcador de arquivo omitido, sua linha de estouro e seu marcador de docs
excluídos; e o cabeçalho de hunk "### File: %s" de budgeting.go. Apenas o
conteúdo do próprio diff REVISADO e o contexto de CI de cada execução (quando
não vazio), o histórico, o contexto da base de código, as notas de memória e o
idioma nunca o movem — esses são lidos apenas dos diffs/opções sentinela fixos
acima, nunca do que runReview de fato recebeu.
TestAUR543B1FixedContentCoversUserHalfAndChangeScope, em
internal/prompt/aur543_test.go, fixa que cada um desses literais está de
fato presente no que fixedContentForDigest renderiza e nomeia o que faltar em
caso de falha. TestAUR543B1DigestIsHashOfFixedContent é o elo que faz essa
checagem de contenção significar algo sobre a chave de cache real: ele fixa
FixedContentDigest() == hex(sha256(fixedContentForDigest())), nada mais.
Juntos: "o literal X está presente no texto renderizado" (contenção) mais "o
digest é de fato um hash puro desse texto renderizado" (o elo) implicam
"editar X move o digest" sem nunca comparar dois valores de digest
diretamente — equivalente a uma comparação antes/depois para cada literal
listado, e mais informativo em caso de falha: nomeia o literal ausente em vez
de apenas reportar "digest mudou". TestAUR543B1ChangeScopeTextMovesDigest
prova adicionalmente, por comparação real antes/depois, que trocar o texto
fixo do próprio ReviewChangeScope (via seu ponto de costura em variável de
pacote) move diretamente o resultado de FixedContentDigest.
Os cabeçalhos literais de string Go de buildUserContent não têm ponto de
costura em processo que um teste possa trocar em tempo de execução; por isso,
AC-001-MUT-002, de tests/acceptance/AUR-543.sh, prova esse caso no nível do
script: ele edita o código-fonte real ("## Code Changes" em builder.go)
numa árvore copiada, depois reexecuta AMBOS os testes como um processo novo e
exige que o teste de contenção fique VERMELHO (o literal sumiu) e que o
teste do elo continue VERDE (a mutação nunca tocou o código de hash do próprio
FixedContentDigest) nessa MESMA árvore mutada. A segunda checagem é o que
elimina o buraco que uma checagem só com contenção VERMELHA deixaria passar:
um FixedContentDigest fixado no valor hex de hoje também faria o teste de
contenção "sobreviver" por coincidência (conteúdo mudou, hash não) sem que a
checagem do elo o pegasse.
O que este digest NÃO cobre, e não pode cobrir: literais que
internal/prompt não possui. Dois pontos de chamada, ambos fora dos paths
deste card (apenas read_paths), acrescentam seu próprio texto fixo pequeno
DEPOIS que PromptBuilder.BuildPrompt retorna, e uma edição em qualquer uma
das duas junções não moveria este digest:
internal/review/reviewer.go(Reviewer.GenerateReview):fullPrompt := promptParts.System + "\n\n" + promptParts.User. A junção de duas quebras de linha é um literal fixo que este digest não lê — ela é reproduzida de forma independente dentro defixedContentForDigest(também"\n\n"), então hoje os dois por acaso concordam, mas nada impõe isso; uma edição na junção do próprio reviewer.go não mudaria a chave do cache.contextInjectingProvider.Complete, deinternal/config/wrap.go:prompt + "\n\n" + p.block. O CONTEÚDO dep.blockanexado já é coberto porcontextBlockCacheDigest(o parâmetrocontextBlockDigestdo próprioreviewContextCacheKey, cmd/aurumcode/review_cache.go), então uma edição em um prompt de repositório/política configurado ou em um arquivo de skill de fato invalida o cache; a junção fixa"\n\n"em si, não.
Os dois resíduos são literais de dois caracteres, não texto instrucional ao qual um modelo reagiria de forma diferente, e um futuro card com escopo em qualquer um dos arquivos poderia incorporar um digest equivalente a partir dali se isso mudar.
runReview (main.go), de cmd/aurumcode, calcula esse digest chamando
newCacheDigestBuilder() (review_cache.go) — um ponto de costura em nível de
pacote que por padrão é prompt.NewPromptBuilder, e não o builder do próprio
revisor, cujo ruleCatalog pode carregar o catálogo dinâmico, expandido por
skills, desta execução. O ponto de costura existe para que
TestAUR543AC001PromptEditForcesFreshReview possa substituir um builder com
conteúdo fixo diferente e provar o AC-001 pela chamada real a runReview — as
entradas de cache de fato invalidando de ponta a ponta —, e não só pelo
resultado unitário do próprio FixedContentDigest; o código de produção nunca
o reatribui. runReview passa o resultado a partitionByCache como o
argumento promptVersion de cache.Key, no lugar da antiga constante
cache.PromptVersion. Usar um builder novo é deliberado, não um descuido: o
catálogo de regras dinâmico/expandido por skills que uma execução ensina ao
modelo já tem seu próprio digest, incorporado separadamente
(ruleCatalogDigest, incorporado ao argumento model de
reviewContextCacheKey); fazer o digest dele uma segunda vez aqui o contaria
em dobro em vez de proteger algo novo. O próprio cache.PromptVersion
continua definido, sem uso pelo código de produção, apenas porque o parâmetro
promptVersion de cache.Key é genérico e tests/unit/AUR-441.go (fora dos
paths deste card) ainda referencia o literal.
Se o cálculo do digest em tempo de execução alguma vez falhar — o template de
produção é compilado via go:embed e sempre faz parse, mas um builder cujo
conjunto de templates está vazio (prompt.NewPromptBuilderWithoutTemplates(),
usado apenas por TestAUR543N1DigestErrorDegradesToNoCache) esbarra no mesmo
erro já publicado, "the review prompt template is unavailable", que
buildBasePrompt devolve a qualquer outro chamador —, runReview incorpora
esse erro em cacheErr exatamente como uma falha de cache.Open: TANTO
partitionByCache (o diff é enviado por inteiro) QUANTO persistFreshResults
(condicionado a cacheErr == nil) são ignorados, de modo que todo o cache
degrada para "sem cache nesta execução" e, criticamente, não escreve NADA sob
uma chave sem sentido — nunca uma falha, nunca uma entrada obsoleta ou errada.
A mutação AC-001-MUT-001 de tests/acceptance/AUR-543.sh faz
FixedContentDigest fazer hash de um literal de string fixo em vez do
conteúdo renderizado real — reproduzindo exatamente o defeito que este card
fecha, um digest que nunca se move por mais que o texto fixo do prompt mude —
e confirma que todo teste TestAUR543AC001* fica VERMELHO, tanto no nível
unitário (internal/prompt) quanto pela fiação real de produção
(TestAUR543AC001PromptEditForcesFreshReview, cmd/aurumcode). Uma segunda
mutação (N1-MUT-001) remove a incorporação cacheErr = promptDigestErr em
main.go, reproduzindo uma falha de digest que silenciosamente NÃO desativa o
cache, e confirma que TestAUR543N1DigestErrorDegradesToNoCache fica
VERMELHO.