ADR-0008: BundleBuilder uses deterministic serial fallback (NOT hedged-race); vuln_index.digest is part of the Bundle cache key¶
Status: Accepted Date: 2026-05-17 Tags: determinism · cache-correctness · commitment-2.4 · veto-strength Related: 0005, 0010, production ADR-0005, production ADR-0030
Context¶
Production design.md §2.4 ("Determinism over probabilism for structural changes") is a veto-strength commitment: "same inputs → same Transform bytes; replay produces identical outputs." Phase 3's BundleBuilder (src/codegenie/plugins/bundle.py) executes TCCM must_read / should_read / may_read queries via language adapters; the question is what happens when an adapter reports AdapterConfidence.Degraded or Unavailable (e.g., stale SCIP index).
The performance lens proposed hedged-race: fire the primary query AND the declared fallback in parallel; return the first-to-complete with confidence weighting. This minimizes p99 latency.
The security and best-practices lenses both proposed declarative serial fallback: invoke the fallback only when the primary returns Degraded or Unavailable. Slightly slower (~+100 ms on degraded paths), but the result is a deterministic function of inputs.
The critic correctly attacked hedged-race in critique.md §Attacks on the performance-first design: hedged-race violates commitment §2.4 by definition — two runs against the same inputs can return different Bundle bytes if the primary wins on run 1 and the fallback wins on run 2 (due to scheduler noise). Different Bundle bytes → different recipe.applies(cve, bundle) decisions → different Transform.diff_bytes → determinism property fails.
Additionally, the critic flagged Hidden Assumption #3: the lens designs' Bundle cache keys did not include vuln_index.digest. A CVE-feed refresh that re-classifies a CVE (e.g., severity rises, affected range widens) must not return a stale cache hit; the cache key must include the vulnerability index's content digest.
Options considered¶
- Option A — Hedged-race in
BundleBuilder(performance lens). Fire primary + fallback in parallel; return first-to-complete weighted by confidence. Pattern: Race-based composition. Violates commitment §2.4. - Option B — Declarative serial fallback;
vuln_index.digestomitted from cache key. Deterministic queries, but stale cache hits on CVE-feed refresh. Pattern: Smart constructor under-specified — cache key doesn't reflect a real input. - Option C — Declarative serial fallback (fire fallback only when primary returns
Degraded/Unavailable), ANDvuln_index.digestincluded in the Bundle cache key. Deterministic + correct under CVE-feed updates. Pattern: Pure Functional core / imperative shell — the Bundle is a fold over typed inputs.
Decision¶
Adopt Option C. BundleBuilder (src/codegenie/plugins/bundle.py) executes TCCM queries under asyncio.Semaphore(min(4, os.cpu_count())) (overridable via CODEGENIE_BUNDLE_CONCURRENCY env var); each query's TCCM-declared fallback runs only when the primary returns AdapterConfidence ∈ {Degraded, Unavailable}. Bundle cache key:
blake3(
plugin_id || plugin_version || primitive || canonicalize(args)
|| repo_ctx.digest || scip.digest || dep_graph.digest
|| vuln_index.digest
)
A property test asserts byte-identical Bundle output across 100 Hypothesis runs on the same inputs.
Tradeoffs¶
| Gain | Cost |
|---|---|
Commitment §2.4 honored at the Bundle layer — Transform determinism property (Goal G4) inherits cleanly |
+~100 ms on degraded paths vs hedged-race max(); acceptable in the 18-s p50 envelope |
vuln_index.digest in cache key means a CVE-feed refresh that re-classifies a CVE invalidates the Bundle cache entry — no stale-cache surprise |
Cache hit rate drops slightly after every codegenie vuln-index refresh; acceptable cost for correctness |
| Hypothesis property test (100 runs, byte-identical output) is feasible because the function IS pure modulo timestamps | The property test is brittle to any non-determinism (set iteration order, dict ordering pre-3.7, hash seed) — strict discipline required |
AdapterDegraded event emission on fallback paths gives clean confidence propagation into TrustOutcome.confidence (Goal G8) |
The fallback path is taken less often in steady state; less-tested code path needs explicit fixture coverage |
CODEGENIE_BUNDLE_CONCURRENCY env var escape hatch addresses the critic's "unbenchmarked SSD-knee" Hidden Assumption #4 — CI tuning without code edits |
Env var sprawl risk; we limit to two env vars total in Phase 3 (this + CODEGENIE_VULN_INDEX_PATH) |
| Cache key includes content digests of every input, not file paths or mtimes — content-addressed cache survives repo moves and clock skew | Cache key computation requires reading all inputs to digest them; amortized by caching the digests themselves |
Postscript (2026-05-19, S3-05 amendment, additive). The Phase-3 env-var
ceiling rises from two to three with the addition of
CODEGENIE_BUNDLE_CACHE_TTL_DAYS — the operator tuning knob for Bundle
cache eviction shipped by S3-05
(Gap 4 fix). The three env vars are now:
CODEGENIE_BUNDLE_CONCURRENCY— original (this ADR).CODEGENIE_VULN_INDEX_PATH— S3-02.CODEGENIE_BUNDLE_CACHE_TTL_DAYS— S3-05 (positive decimal integer days; default7). Malformed values raiseBundleCacheRaise(reason="invalid_ttl_env")— silent default-fallback hides operator typos (Rule 12).
Pattern fit¶
Implements Functional core / imperative shell (toolkit §Architecture-scale patterns) — the Bundle is a pure fold over typed inputs (plugin_id, repo_ctx.digest, vuln_index.digest, primitive queries' canonicalized args), with side effects (adapter dispatch, disk reads) at the edges. The cache key IS the content-hash of the inputs; identical inputs guarantee identical outputs. Rejects hedged-race composition because it introduces scheduler-dependent non-determinism into a function that must be pure.
Consequences¶
src/codegenie/plugins/bundle.pyshipsBundleBuilderwithasyncio.Semaphore(min(4, os.cpu_count()))and serial fallback dispatch.tests/unit/plugins/test_bundle.pyasserts (a) Bundle cache hit on identical inputs, (b) cache miss aftervuln_index.digestchange, (c) fallback fires deterministically (not raced) onDegraded.- Property test: 100 Hypothesis runs of
BundleBuilder.build(...)against the same inputs → byte-identical Bundle. AdapterDegradedevents on the workflow-internal stream carry(primitive, adapter_name, reason)payload;TrustScorerfolds them intoTrustOutcome.confidence.CODEGENIE_BUNDLE_CONCURRENCYenv var is the single tuning knob; documented in operator runbook.- Cache-key collision is impossible by construction (blake3 hash of all inputs); a key mismatch indicates a real input change.
BundleCacheGchelper (per architecture-spec Gap 4) runs at orchestrator init iftime.time() - last_gc > 86400; operator aliascodegenie cache pruneinvokes unconditionally.- Phase 8's Redis hot views replace this cache; the cache-key shape (blake3 over content) ports unchanged.
Reversibility¶
Low (for hedged-race). Switching to hedged-race violates commitment §2.4 directly — the determinism property test would fail by construction. The only reversible direction is more determinism (e.g., synchronous serial without the semaphore), not less.
High (for cache-key shape). Adding additional digest inputs (e.g., a future ADR ships policy.digest) is mechanical; removing an input would invalidate every existing cache entry but is a one-line change.
Evidence / sources¶
../phase-arch-design.md §Component design C7, §Patterns considered and deliberately rejected ("No hedged-race inBundleBuilder"), §Goals G4 + G8../final-design.md §Synthesis ledger rows "BundleBuilder fallback semantics"(score 15/15) and "vuln_index.digestin Bundle cache key" (score 15/15)../critique.md §Attacks on the performance-first design — hedged-race violates §2.4, §Hidden assumptions #3 (vuln_index.digest), Hidden assumptions #4 (unbenchmarked SSD-knee)docs/production/design.md §2.4— determinism over probabilism (veto-strength commitment)- production ADR-0005 — no LLM in gather pipeline
- production ADR-0030 — graph-aware context queries
- design-patterns-toolkit.md §Functional core, imperative shell