ADR-024: Abstraction Canonicalization and Import-Time Boundary Guard¶
Metadata¶
- Status: Proposed
- Date: 2026-07-29
- Decision Makers: Vijaykumar Singh
- Related ADRs: 007 (contracts boundary — the layering this hardens), 002 (state management),
019 (decomposition — a smaller runtime makes canonicalization tractable) - Work tracked by: TD-26
- Benchmark: competitive-benchmark-2026-07.md §4 (scoped state)
Context¶
Two review findings, one principle:
-
Duplicated abstractions per concern. Responsibility for the same job is spread across several
near-synonymous surfaces, so "which one is canonical?" is unclear at import time:
- Provider construction/lookup:victor/providers/factory.py+victor/providers/registry.py- the runtime
ProviderService+ the orchestrator'sProviderManagercomponent. - State:
GlobalStateManager(state/global_state_manager.py) +state/managers.py+
state/factory.py. - Caching:
cache_manager.py+query_cache.py+embedding_cache_manager.py.
Some of this is legitimate layering (a registry vs a service vs a facade); some is genuine
duplication. Today nothing declares which surface new code should import.
- the runtime
-
Boundaries are enforced post-hoc. The architectural guards
(test_architectural_boundaries.py,test_core_vertical_import_boundary.py,
test_service_layer_validation.py) parse the AST at test time. A developer can violate a
boundary locally and only learn at CI — after the bad import has spread.
Decision¶
- One canonical surface per concern, declared and documented. For each of provider
construction, state management, and caching: name the single canonical entry point, document the
role of every other surface (or delete it), and record the mapping inarchitecture.md's
Additional Subsystems section. Where two surfaces are genuinely one, collapse them; where they are
distinct layers, the doc says so and points at the canonical one to import. - Import-time boundary guard. Add an opt-in import hook (a
sys.meta_pathfinder or an
equivalent pytest/conftest import assertion) that fails at import when a forbidden cross-layer
import is executed — turning the existing AST rules into a fail-fast signal in dev, with the
post-hoc AST tests retained as the CI backstop. The guard reuses the boundary rules the AST tests
already encode (single source of truth for the rules).
This is a canonicalization + enforcement decision, not a rewrite: no behavior changes, only which
surface is authoritative and when violations surface.
Rationale¶
- First principles. An abstraction earns its keep only if there is exactly one obvious way to
reach a capability. Three registries for one concern is negative leverage — every reader pays the
"which one?" tax and imports drift. - Fail fast. A boundary caught at import is a boundary the developer fixes in seconds; one caught
at CI is one that has already propagated. Same rules, earlier signal. - Co-design. The import guard consumes the same rule set the AST tests use, so doc, dev-time
guard, and CI guard cannot disagree.
Consequences¶
- Positive: unambiguous imports; drift caught in the editor, not at CI; a cleaner target for
ADR-019's decomposition (fewer places provider/state logic can hide). - Negative: the import hook must be cheap and must not break lazy-import startup performance
(the CLI relies on lazy heavy imports); mis-scoped rules could raise false positives — hence
opt-in in dev, AST tests remain the authority in CI. - Neutral: public APIs unchanged; verticals already constrained to
victor_contracts.
Implementation¶
Internal — no companion FEP (no public-API change):
- Inventory each concern's surfaces; write the canonical-surface table into
architecture.md;
collapse true duplicates, document true layers. - Factor the boundary rules into a single shared rule module consumed by both the AST tests and a
new import-time guard (conftest/sys.meta_path), opt-in via an env flag for dev. - Add a hygiene assertion that new code imports the canonical surface (extend
repo_hygiene_check.pywhere cheap).
Alternatives Considered¶
- Leave duplication, rely on convention. Rejected: convention is exactly what drifted here.
- Hard import-time enforcement always-on in CI too. Deferred: risk to lazy-import startup and
false positives; keep AST tests as the CI authority, guard as the dev fast-path. - Full DI container. Rejected: heavier than the problem; the six services +
ExecutionContext
already provide the seam — this ADR just makes the entry points singular and enforced.
References¶
- ADR-002, ADR-007,
ADR-019 victor/providers/{factory,registry}.py,victor/state/{global_state_manager,managers,factory}.py,
tests/unit/framework/test_architectural_boundaries.py,
tests/unit/contracts/test_core_vertical_import_boundary.py
Revision History¶
| Date | Version | Changes | Author |
|---|---|---|---|
| 2026-07-29 | 1.0 | Initial ADR — canonical surface per concern + import-time boundary guard | Vijaykumar Singh |