Core Activity Representation and Envelope Reconstitution
Context and Problem Statement
Vultron is a set of communicating core state machines. Each Actor is an isolated process: it owns a DataLayer of core domain objects, and (per the Actor Knowledge Model) its knowledge of the world is bounded strictly by what it has received. No actor can reach into another actor's DataLayer. Wire (AS2) exists solely as the transport by which one actor's core state informs another's — an AS2 Activity is an envelope carrying a core domain fact across the process boundary, not a domain object in its own right.
ADR-0017 established that wire is a projection of core, and ARCH-09-001
requires core models to be as rich as or richer than their wire counterparts.
The 29 AS2 protocol Activities (Offer, Invite, Accept, Reject, …) break
this: they were built wire-first and were never given a core counterpart. For
these types the projection is inverted — wire is the only representation,
and is therefore richer than core.
The concrete consequence: when an actor must remember what a message told it
(the case id an Invite carried, the recommender an Offer named, the report
embedded in a submit Offer, the pending embargo proposal for a case), there is
no core object to hold that fact, so core reaches back through the DataLayer
and re-reads the stored wire envelope — dl.read(activity_id) inside
vultron/core/. Concern #1506 audited these sites. ADR-0034 recognised activity
read-back as a boundary violation but explicitly scoped it out and deferred
it to a separate concern; this ADR is that concern's decision.
The dl.read(activity_id) call in core is a symptom. The root cause is a
missing layer of core domain representation for protocol facts. A second,
subtler question surfaced during analysis: emitting a reply (e.g. an Accept)
requires the full inline verbatim original activity in the reply's object_
(the Actor Knowledge Model forbids bare URIs), and activity ids are
non-regenerable random urn:uuid: values — so the original envelope cannot be
losslessly reconstructed from core state. Does that legitimise reading a stored
activity back?
Decision Drivers
- Restore "wire is a projection of core" (ADR-0017) and ARCH-09-001: every domain fact an actor remembers MUST have a home in core.
- Honour ARCH-03-001 (AS2↔domain mapping happens in exactly one place, the semantic extractor) — core MUST NOT re-interpret an activity a second time.
- Honour ARCH-01-002 / DL-05: wire vocabulary types MUST NOT enter core through the persistence port.
- Do not regress into cloning each AS2 Activity into a 1:1 core Activity class —
the generic-event-mirroring-AS2 anti-pattern
notes/domain-model-separation.mdwarns against. - Satisfy the Actor Knowledge Model's requirement that replies embed the full inline original activity, given that activity ids are not regenerable.
- Keep the migration measurable and regression-proof via the DL-05-004 ratchet.
Considered Options
- A. Give protocol facts a core representation; correlate via core entities; confine verbatim reconstitution to a wire/adapter seam. The extractor records the domain fact each inbound message carries as core state (a transition on an existing core entity or a purpose-built core record) at interpretation time. Core reads that core state; correlation between messages (Accept→Invite) flows through a core-entity relationship. Verbatim envelope reconstitution for replies reads a stored activity back only through a wire/adapter-owned seam that treats the payload as opaque.
- B. Clone each AS2 Activity into a 1:1 core Activity class. Give every
protocol message a mirror core type and read that instead. Rejected: this
duplicates the wire envelope into core rather than modelling the domain fact,
reproduces AS2-shaped fields (
object_,target,context) in core vocabulary, and is the exact anti-patternnotes/domain-model-separation.mdcautions against. It relocates the violation without curing it. - C. Pure read-relocation / lossless re-projection. Stop storing activities;
reconstruct any needed envelope by re-projecting core state through the
serializer on demand. Rejected: activity ids are random
urn:uuid:values, so re-projection produces a different id and serialization every time — it cannot reproduce the verbatim original the Actor Knowledge Model requires for replyobject_. Lossless reconstitution from core state is not achievable.
Decision Outcome
Chosen option: A, because it is the only option that gives protocol facts a true core home (curing the ARCH-09-001 inversion rather than relocating it, rejecting B) while still satisfying the Actor Knowledge Model's verbatim-reply requirement given non-regenerable ids (which C cannot meet).
The decision splits two needs that dl.read(activity_id) conflates today:
| Need | Source of truth | Rule |
|---|---|---|
| Semantic content — what a message means (case id, embargo id, recommender, embedded report) | Core state | MUST come from a core entity; core MUST NOT re-read the activity to interpret it (DL-06-001, DL-06-002). |
| Correlation — which prior message this one answers | Core-entity relationship | MUST resolve through a domain relationship, not a wire re-read (DL-06-003). |
Envelope reconstitution — embedding the verbatim original in a reply's object_ / in_reply_to |
Stored opaque activity payload | MAY read a stored activity, but only via a wire/adapter-owned seam that never interprets it (DL-06-004). |
Decision details
- Protocol facts get a core representation. The semantic extractor — the single interpretation site (ARCH-03-001) — records each domain fact a received message carries as core state at interpretation time, either as a state transition on an existing core entity or as a purpose-built core record. This is not a 1:1 clone of the AS2 Activity: model only the domain fact, in domain vocabulary, capturing only what handlers use (DL-06-002).
- Core reads facts from core state, never from the wire envelope. Core code
MUST NOT call
dl.read(activity_id)/dl.list_objects(<activity_type>)to recover semantic content (DL-06-001). - Correlation is a core-entity relationship. When a message references a prior one, core correlates through the core entity the prior message created or updated — not by re-reading the referenced activity (DL-06-003).
- Verbatim reconstitution is a wire/adapter seam. Reading a stored activity
payload back to embed the verbatim original in an outbound reply is permitted,
but owned by the wire/adapter layer and treated as opaque; core never reads it
to interpret meaning. Such a seam is not counted as a core boundary
violation under DL-05-004 (DL-06-004).
CaseLedgerEntry.payloadSnapshotis the existing precedent for opaque, write-only activity retention. - The ratchet shrinks to zero. As each core semantic read is migrated off activity read-back, its AS2 Activity type is removed from the DL-05-004 exemption set; the set MUST reach zero, leaving only the DL-06-004 seam (DL-06-005).
The migration itself is out of scope for this decision — this concern's
deliverable is the audit and the implementation issues. The audited sites and
their classification are recorded in notes/datalayer-design.md.
Consequences
- Good, because protocol facts gain a canonical core home, restoring "wire is a
projection of core" (ADR-0017, ARCH-09-001) and removing the runtime
core → wiredependency (ARCH-01-002). - Good, because interpretation happens exactly once, at the extractor (ARCH-03-001); core stops maintaining a second, drifting reading of the same message.
- Good, because the verbatim-reply requirement is met honestly (via retained opaque envelopes) instead of via unachievable lossless re-projection.
- Good, because the DL-05-004 ratchet gives the migration a measurable, regression-proof target of zero core semantic activity reads.
- Neutral, because activities may still be persisted — as opaque audit/ledger/reply-envelope blobs — provided core never interprets them.
- Bad, because the migration is wide (report, embargo, actor/participant subsystems) and staged across multiple implementation issues; the exemption set shrinks incrementally rather than at once.
Validation
- The DL-05-004 architecture ratchet asserts no
vultron.wire.as2vocabulary type escapesdl.read()/dl.list_objects()intovultron/core/, with a shrink-only exemption set; DL-06-005 ties the exemption set to zero. test/architecture/test_core_no_wire_imports.pycontinues to enforce that core does not import wire.- Per-migration PRs remove the migrated Activity type from the exemption set and add tests that the domain fact is read from core state.
More Information
- Concern #1506 — the driving audit (core reads wire AS2 Activities back from the DataLayer); parent Epic #1394 (Architecture Hardening).
- ADR-0034 — DataLayer Port Returns Core Domain Objects; recognised activity read-back as a tracked separate concern (this one) and established the core-object read contract (DL-05) this ADR extends.
- ADR-0017 — Domain/Wire Object Separation; "wire is a projection of core".
- ADR-0009 — Hexagonal Architecture; the enclosing ARCH-01/03/09 rules.
- ADR-0019 — Separate the Case Ledger from the Per-Actor Process Log;
payloadSnapshotprecedent for opaque activity retention. notes/datalayer-design.md§ "Read Path MUST Return Core Objects" and § "Activity Read-Back: Semantic Content vs. Envelope Reconstitution".notes/domain-model-separation.md— domain events as the bridge; the anti-pattern of cloning AS2 shapes into core.
Generated spec requirements: datalayer.yaml DL-06-001 through DL-06-005.