DataLayer Port Returns Core Domain Objects
Context and Problem Statement
The SQLite DataLayer adapter reconstructs persisted objects through the wire
vocabulary registry (find_in_vocabulary() in
vultron/adapters/driven/db_record.py). As a result, dl.read() and
dl.list_objects() return wire-layer types (e.g. as_VulnerabilityCase
from vultron/wire/as2/vocab/objects/) rather than core domain types
(e.g. VulnerabilityCase from vultron/core/models/).
Core BT nodes and use cases call dl.read() and operate on the result. To
avoid a compile-time core → wire import (forbidden by ARCH-01-001), core
depends on structural duck-typing Protocols and TypeGuard helpers in
vultron/core/models/protocols.py (CaseModel, is_case_model(), etc.).
These let core call methods on wire objects at runtime without importing the
wire classes. The boundary "works" only because wire and core types share
field names and the same type_ string value.
This produces three problems:
- Core validators never run on stored data. A wire object satisfies core call sites by structural coincidence; core's domain invariants are never enforced on DataLayer-sourced data.
- The Protocols evade rather than honour ARCH-01-001. They hide a runtime
core → wiredependency from static analysis; mypy/pyright pass while core operates on wire objects. - The seam is fragile. After ADR-0017 / issue #1387 renamed wire classes
to the
as_prefix (as_VulnerabilityCase), the classes are now genuinely distinct — but the runtime boundary violation remains, held together only by the sharedtype_string and field names.
It was also unclear where wire↔core translation belongs. This ADR records that decision. Source concern: #1496 (parent Epic #1394).
Decision Drivers
- Restore the hexagonal boundary: core functions MUST accept and return domain types only (ARCH-01-002); core MUST NOT import wire (ARCH-01-001).
- Keep the wire vocabulary registry a wire-layer concern (DL-03-002); the DataLayer must not depend on it to reconstruct core objects.
- Prefer a translation boundary that is enforced by types and a ratchet, not by structural coincidence.
- Keep the change reversible in stages and reviewable (the core-side cleanup touches ~45 files).
Considered Options
- A. Adapter reconstructs and returns core objects natively. The DataLayer
read path reconstructs persisted domain entities via the core vocabulary
registry (
CORE_VOCABULARY).dl.read()/dl.list_objects()return core types. Wire types never enter core. The adapter may still use wire types internally, but the port contract is a core object. - B. Use cases convert wire → core at each boundary. DataLayer keeps
returning wire objects; each core site calls
model_validateto convert. Rejected: scatters conversion across ~45+ sites and is explicitly prohibited by DL-01-004 (nomodel_validateondl.read()results in core). - C. Physically split into a wire-owned DataLayer and a core-owned DataLayer. Two persistence scopes. Rejected for this decision: changes persistence topology and per-actor isolation (ADR-0012); far larger than the concern requires. Left as possible future work.
Decision Outcome
Chosen option: A — the adapter reconstructs and returns core objects natively, because it is the only option that satisfies ARCH-01-002 without scattering conversion logic (rejecting B) and without re-architecting persistence topology (rejecting C).
Decision details
- Port contract is a core object. For every persisted
type_that has a registered core counterpart,dl.read(id)anddl.list_objects(type_key)return an instance of the corevultron/core/modelsclass, reconstructed viaCORE_VOCABULARY(DL-05-001). - The adapter owns wire↔core translation and maintains its own
type_→core-class mapping independent of the wireVOCABULARYregistry (DL-05-002, DL-03-002). Whether the adapter uses wire types as intermediate values internally is an implementation detail hidden behind the port. - The duck-typing Protocols are removed. Once reads return core objects,
core depends on concrete core classes (real
isinstancenarrowing); the workaround Protocols andTypeGuardhelpers invultron/core/models/protocols.pyare deleted (DL-05-003). - AS2 Activities are the recognised exception. Persisted AS2 Activity types
(the protocol message types in
vultron/wire/as2/vocab/activities/) have no core counterpart, so they cannot be returned as core objects. Core code that reads stored wire Activities back from the DataLayer (e.g. reading a storedas_Offer) is itself a boundary violation, but migrating it out of core is out of scope for this decision and tracked as a separate concern. Until then, the ratchet exemption set (DL-05-004) enumerates these types explicitly so it can only shrink. - A ratchet test enforces the contract (DL-05-004): no
vultron.wire.as2vocabulary type may be returned fromdl.read()/dl.list_objects()intovultron/core/, except the enumerated Activity exemptions.
Consequences
- Good, because core validators run on DataLayer-sourced data and the wire→core runtime dependency disappears (ARCH-01-001, ARCH-01-002).
- Good, because the DataLayer stops depending on the wire vocabulary registry to reconstruct core objects, so the wire layer can evolve independently (DL-03-002).
- Good, because ~70 files of duck-typing scaffolding collapse to concrete-type usage, restoring static-analysis visibility of the boundary.
- Neutral, because the adapter may retain wire types internally; only the port contract changes.
- Bad, because the core-side cleanup is wide (~45 files using guards, ~25 using Protocol hints) and is staged across multiple implementation issues.
- Bad, because Activity read-back from core remains a known, tracked violation until a follow-on concern migrates it.
Validation
- An architecture ratchet test asserts no
vultron.wire.as2vocabulary type escapesdl.read()/dl.list_objects()intovultron/core/, with an enumerated (shrink-only) exemption set for AS2 Activity types (DL-05-004). test/architecture/test_core_no_wire_imports.pycontinues to enforce that core does not import wire.- Existing DataLayer round-trip tests confirm that a saved core object reads back as the same core type.
More Information
- Concern #1496 — the driving report (DataLayer returns wire objects to core).
- Epic #1394 — Architecture Hardening; parent of the implementation work.
- ADR-0017 — Domain/Wire Object Separation (shared-base, two-branch hierarchy); established the distinct core and wire class hierarchies this ADR relies on.
- ADR-0009 — Hexagonal Architecture; the enclosing rule (ARCH-01-001, ARCH-01-002) this ADR operationalizes for the persistence port.
- ADR-0012 — Per-Actor DataLayer Isolation; relevant to the rejected Option C.
- Generated spec requirements:
datalayer.yamlDL-05-001 through DL-05-004. notes/datalayer-design.md§ "Vocabulary Registry Entanglement Across Wire, Core, and DataLayer" — the analysis this decision resolves.