ADR-0006: Adapter dispatch order is an explicit Final tuple, not implicit dict.items() iteration¶
Status: Accepted Date: 2026-05-19 Tags: strategy-via-data · determinism · critic-bp1 · final-tuple Related: 0007, 0004, production ADR-0033, production ADR-0038
Context¶
Production ADR-0038 defers the adapter-chain-assembly policy question. The three lens designs answered it differently:
- Performance-first proposed a
VulnProvenanceChainAssemblerclass with runtimeconfidence × cost_band × applies_whenordering — and admitted in the same paragraph that adding a new adapter family requires editing the class. CoR claim, Strategy reality. - Security-first proposed a
chain.pyclass with deterministic order plus aSbomVerifiercross-check. Over-engineered for two adapters. - Best-practices proposed a free function
assemble_provenance(...)that walkedregistry.items()— and acknowledged in Acknowledged blind spots #5 that "'declared order' semantics depend ondictinsertion order." That is a load-bearing global-state ordering invariant smuggled in via implementation detail. The order an adapter registers is the order plugins import, which depends on plugin-loader iteration order, which depends on filesystem ordering orsorted()discipline neither of which is specified. The critic landed BP-1 hard.
final-design.md §Synthesis ledger departure #1 and §Synthesis ledger row 13 (score 15/15) lock the answer: dispatch order is explicit data, declared in code as a module-level Final tuple, decoupled from registration order.
Options considered¶
- Option A — Runtime sort by
confidence × cost_band × applies_when. Performance-first. Pattern: Strategy with runtime ordering. Extends the adapterProtocolwith two new fields; addingRhelVulnProvenanceAdapterrequires editing the chain assembler. - Option B — Implicit
for k, v in registry.items()iteration. Best-practices. Pattern: Implicit-order-as-policy. Smugglesdictinsertion order (which is plugin-import order) as the dispatch policy. - Option C — Module-level
Finaltuple_ADAPTER_DISPATCH_ORDERdeclaring the layer-set walk order; within a layer-set, iterate inEcosystem-enum-sorted order. Pattern: Strategy via data — data declares policy; code reads it.
Decision¶
Adopt Option C. src/codegenie/primitives/vuln_provenance/assembly.py declares at module top:
_ADAPTER_DISPATCH_ORDER: Final[tuple[tuple[Layer, ...], ...]] = (
(Layer.APP,), # app-layer adapters first
(Layer.BASE_IMAGE,), # then base-image adapters
(Layer.RUNTIME,), # then runtime-bundled adapters
)
assemble_provenance(...) walks _ADAPTER_DISPATCH_ORDER; within each layer-set, it iterates the registry in Ecosystem-enum-sorted order (not dict.items() order). Adding a new Layer family (e.g., Layer.SIDECAR) requires touching this tuple — explicit, ADR-worthy. Adding a new Ecosystem to an existing layer is free (registry-only). Registration order is not load-bearing.
Tradeoffs¶
| Gain | Cost |
|---|---|
| Closes critic BP-1: registration order, plugin-import order, and filesystem iteration order are no longer load-bearing for routing | Adding a new Layer.SIDECAR (or similar) is genuinely cross-cutting and requires touching the tuple + an ADR — slightly more friction than "just register an adapter" |
| Operators predict behavior by reading one tuple of three rows; no DSL, no class hierarchy, no runtime sort | The tuple is hardcoded in the assembly module; rebuilding the dispatch order at runtime (e.g., per-CVE-class — performance's [P-v33]) is rejected. Acceptable per critic ("premature pluggability") |
A property test (tests/property/vuln_provenance/test_dispatch_order_invariant.py) shuffles registration order across 50 permutations and asserts assemble_provenance result is byte-identical — locks the discipline at the property level |
The property test is non-trivial (Hypothesis-driven) but is high-signal; the test is on the load-bearing roadmap-coherence path |
Within a layer, Ecosystem-enum-sorted order is deterministic across runs (no set or dict.items() order) — operators reason about polyglot tiebreakers by reading the Ecosystem enum definition |
Adding a new Ecosystem value re-orders the within-layer iteration if it sorts before existing values; mitigated by the practical observation that polyglot tiebreakers are rare and the property test pins the order |
Fits Strategy via data (toolkit) — data declares policy, code reads it; no if/elif branches on layer |
One extra indirection: the dispatch loop walks two levels (layer-set, then within-layer); slightly harder to single-step in a debugger than a flat loop |
Pattern fit¶
Implements Strategy via data (toolkit §Behavioral — Strategy expressed as a typed table, not a parallel class hierarchy): the dispatch policy is a typed Final tuple iterated at one call site; new policies are data additions, not code branches. Also instantiates Final-tuple marker catalog — the codebase's existing pattern (_GENERATOR_HEADER_MARKERS, _REFLECTION_QUERIES, _LOCKFILE_PRECEDENCE) used for similar dispatch tables. Newtype + sum-type discipline per production ADR-0033.
Consequences¶
_ADAPTER_DISPATCH_ORDER: Final[tuple[tuple[Layer, ...], ...]]is the module-level constant inassembly.py. Any change requires an ADR amendment.- The
(Layer.RUNTIME,)row is reserved (Phase 7 ships no runtime adapter). First runtime adapter (JRE-bundled, future phase) exercises it; Phase 7 includes a property test asserting the empty runtime layer behaves correctly. - Within-layer iteration is
sorted(adapters_for_layer, key=lambda a: a.ecosystem)— explicit, byEcosystemenum value. assemble_provenanceusesmatch/assert_neveron(app_result, base_result)(critic BP-4) — noif r.kind in {"app_direct", "app_transitive", ...}string-set comparisons.- Property test
test_dispatch_order_invariant.pyshuffles registration order across 50 permutations and asserts byte-identical results. - Property test
test_both_invariant.pyasserts that for any(AppKind, BaseKind)pair where both are non-Unknown,assemble_provenancereturnsBoth(app_record, base_record)with no recursion. - Adding a new
Layervalue to the tuple is an ADR-worthy event; the assembly module's docstring lists this as the extension protocol. - Performance-first's
[P-v9]adapter Protocol extension withcost_band + applies_whenis rejected as kernel-contract drift; see 0007 for the registry-side discipline.
Reversibility¶
Medium. The constant's name (_ADAPTER_DISPATCH_ORDER) and shape are internal to the assembly module; changing the shape (e.g., adding per-CVE-class dispatch) is one file's change, plus a property test update. However, downstream consumers (Phase 8's Planner) read assemble_provenance outputs without caring about the dispatch order itself, so the shape change does not propagate. The discipline of "registration order is not load-bearing" is what's truly hard to reverse — operators would have to be re-trained, and a regression that re-introduced implicit ordering would be a silent correctness bug.
Evidence / sources¶
../final-design.md §Lens summary(non-obvious choice #2), §Synthesis ledger row 6 + row 13 (score 15/15), §Departures from all three inputs #1../phase-arch-design.md §Component design §5(_ADAPTER_DISPATCH_ORDER), §Component design §6 (assemble_provenance(...))../critique.md §Attacks on the best-practices design §1(BP-1 —dict.items()smuggles registration order), §Anti-patterns from the toolkit's "flag on sight" list (Stringly-typed identifiers, Premature pluggability)- production ADR-0038 — Vulnerability provenance attribution
- production ADR-0033 — Domain modeling discipline