Pular para conteúdo

AUR-441 — aurumcode review --base <ref>: do not pay twice for the same file

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

Command

aurumcode review --base <ref> [--fail-on <level>] [--modelo <nome>] [--seguranca]

No new flag. On the --base path, a second invocation against an unchanged file does not resend that file to the model, and the command reports how many files were reused. --pr's review path (AUR-438) is unaffected. Every other published behavior — output shape, exit codes, --fail-on, --modelo, --seguranca — is unchanged.

Why this exists

The review engine sends the entire reviewed diff to the model in one prompt, one Complete call per invocation (internal/review.Reviewer.GenerateReview, internal/prompt.PromptBuilder.BuildPrompt): there is no per-file send for a cache to intercept. cmd/aurumcode (cmd/aurumcode/review_cache.go) filters the diff down to the files internal/review/cache does not already hold an entry for before calling GenerateReview; when nothing missed, the call to the model is skipped entirely.

Cache key and storage

Each changed file gets its own cache key: a content digest of that file's own diff (path, language tag, every hunk — the only bytes the engine bases a finding on), combined with the model identity and a prompt-version constant. Any change to what the model would see for that file — a byte in the diff, the model, or a manually-bumped prompt version — misses.

The digest is computed over the raw, unredacted diff content, not the redacted form actually sent to the model: redacting first would collapse two genuinely different file versions that differ only inside a redacted span (a rotated secret, for instance) into the same key, masking a real content change. A digest is one-way, so nothing recoverable is persisted by computing it this way.

"Model identity" is not simply the provider's name:

  • The offline fixture provider (AURUMCODE_LLM_FIXTURE) reports the same name regardless of which canned response file is configured, but two different fixture files answer differently — so, for caching, they are two different models: the fixture file's own content digest is folded into the key whenever AURUMCODE_LLM_FIXTURE is set.
  • --limite (AUR-433) wraps the provider to pin the cost tracker's accounting key; that wrapper's resolved model key (llm.ModelResolver) is folded in too, so a metered and an unmetered request against identical content never share an entry — a metered call is guarded by a budget check an unmetered one never has.

A live, unwrapped, unfixtured provider's answer is determined by provider.Name() alone, which is used verbatim.

Entries live one file per key under a cache directory:

  • AURUMCODE_CACHE_DIR, when set, names it exactly (even a relative path): two invocations that name the same directory reuse each other's entries.
  • Otherwise, a directory unique to this process invocation ($TMPDIR/aurumcode-review-cache-<pid>) — never a location a separate aurumcode run could land on by coincidence, so a bare, unconfigured second run behaves exactly as if caching were off. This was a deliberate reversal from a first design that scoped the default to the reviewed repository (so a bare second run would reuse it automatically): that design broke AUR-433's already-published --limite contract, whose own acceptance program runs the built binary several times in a row against one repository — once unmetered, twice at an identical --limite ceiling to prove byte-for-byte determinism, once refused — and asserts every call independently reaches the model and reports its own real, freshly-computed cost. A shared default would have served the second --limite invocation from the first's cache entry: no call, an "actual cost" of $0.0000 instead of the real figure the first invocation printed, and a determinism assertion that can never pass again by construction, because the two invocations would no longer be doing the same work. Persisted reuse across separate invocations is therefore something the caller always asks for explicitly, via AURUMCODE_CACHE_DIR — exactly what every proof of this card's own Outcome does.

A cache directory that cannot be created or used degrades the run to exactly the pre-AUR-441 behavior: every file sent, every run. Caching is a performance optimization, never a correctness gate — a write failure never fails an otherwise-successful review.

Output shape

Stdout is byte-identical whether or not caching engaged, in a cold run or a fully-reused one: a cache hit's findings are merged back into the same printed result a fresh call would have produced. The one visible signal is on stderr, and only when at least one file was actually reused:

aurumcode review: reused <N> file(s) from cache (not resent to the model)

This mirrors --fail-on's and --modelo's existing stderr-only notes: the published stdout contract never grows a new line for a flag- or state-dependent behavior.

--fail-on composes correctly with a cache hit: the gate reads the merged result, so a reused finding still closes the gate exactly like a freshly produced one. --seguranca's deterministic pass is unaffected — it always scans the full diff and never consults the cache.

Exit codes

Unchanged from docs/specs/AUR-430.md, AUR-431.md, AUR-436.md:

Code Meaning
0 Review ran; gate (if any) open.
1 The review itself failed.
2 Usage error.
3 --fail-on gate closed (a cache-hit finding counts).

Offline, secret-free example

cd tests/fixtures/repos/git-demo/repo.git
export AURUMCODE_LLM_FIXTURE=../../review/known-problem-response.json
export AURUMCODE_CACHE_DIR=/tmp/example-aurumcode-cache

aurumcode review --base HEAD~1
# config/demo-tokens.txt:4: [error] ... (rule security/hardcoded-secret: Hardcoded Secrets)

aurumcode review --base HEAD~1
# config/demo-tokens.txt:4: [error] ... (rule security/hardcoded-secret: Hardcoded Secrets)
# stderr: aurumcode review: reused 2 file(s) from cache (not resent to the model)

The second run's stdout is byte-identical to the first; the reuse note appears only on stderr.

Failure diagnosis

  • A second run never reuses anything (stderr never shows reused ...) — without AURUMCODE_CACHE_DIR set, every invocation gets its own process-scoped cache and always behaves like a cold run; set AURUMCODE_CACHE_DIR to the same directory across invocations to observe reuse.
  • A reused file's finding is missing from the output — check that AURUMCODE_CACHE_DIR is actually writable and was not cleared between runs; a cache miss simply resends the file, it never drops a finding.
  • Two different AURUMCODE_LLM_FIXTURE files against the same repository correctly produce two different results, never a stale answer from the other fixture: they are treated as two different models for caching.
  • A metered (--limite) and an unmetered run sharing an explicit AURUMCODE_CACHE_DIR correctly never serve one another's cached answer: they are treated as two different models for caching (see "Cache key and storage").
  • --base HEAD (nothing changed) still calls the model exactly like before AUR-441; a zero-file diff is not treated as a full cache hit.

Security

  • Whatever is written to a cache file already passed internal/review.redactReviewResult (AUR-432) before this card's own code ever saw it; nothing model-authored reaches disk unredacted.
  • The cache key is a one-way digest of raw content, never the content itself, so nothing recoverable is persisted by key computation either.
  • AURUM_SECRET_CANARY never appears in stdout, stderr, prompt, or any cache file; tests/acceptance/AUR-441.sh and tests/integration/AUR-441.go assert this on every run, including walking every file the cache directory contains.

Validation

tests/acceptance/AUR-441.sh AC-001 (sealed: ./.board/bin/oci-run --profile bootstrap-readonly-v1 --card AUR-441), with selectors TestAUR441 (tests/unit/AUR-441.go), IntegrationAUR441 (tests/integration/AUR-441.go), E2EAUR441 (tests/e2e/AUR-441.sh) and AC-001-MUT-001 (invalidating reuse on every execution, which must make the submission count stop dropping between runs).