A Ledger Entry's Semantic Type Is the Registry Label of Its AS2 Payload
Context and Problem Statement
Every case ledger entry carries two descriptions of one act.
Its payloadSnapshot holds an ActivityStreams 2.0 (AS2) activity, such as Invite(VulnerabilityCase) or Add(ParticipantStatus).
Its eventType holds a semantic type, a name for what that activity means, such as invite_actor_to_case.
The semantic registry (vultron/semantic_registry/) already maps a wire activity's shape to its semantic type for received messages, and the message vocabulary docs are generated from it (ADR-0128).
The ledger does not use it.
Each write path stamps eventType from its own string constant.
About ten of those strings name semantic types the registry does not know, for acts the CASE_MANAGER records on its own, such as creating a participant record, fully closing a case, or the lapse of an embargo invitation.
One payload uses an activity type that is not AS2 at all.
A reader of a ledger then has no single source for what an entry means. The demo UI (ADR-0104) shows the consequence: it keeps its own closed list of event names, and that list had already drifted from what the code writes when it was audited.
The question this record settles is what an entry's meaning is, and where it comes from.
Decision Drivers
- Vultron's vocabulary should be one known set: every AS2 activity Vultron can send, receive, or write to a ledger, each with one semantic type.
- Even the CASE_MANAGER's own acts are expressible as AS2 activities, so they need no separate naming scheme.
- Replicas route ledger replay on
eventTypealone, and that should stay cheap. - Readers outside Python, such as the demo UI and the report tool, need the vocabulary as data they can check against.
Considered Options
- The payload is the only source;
eventTypeis dropped and every reader classifies the payload eventTypestays an independent label from a closed list, and the payload is unconstrained- The payload is valid AS2, and
eventTypeis stored but must equal the registry's label for it
Decision Outcome
Chosen option: "The payload is valid AS2, and eventType is stored but must equal the registry's label for it", because it gives the ledger one source of meaning without making every replica run the pattern matcher on replay.
- The registry is Vultron's complete vocabulary. It labels every AS2 activity shape that Vultron can send, receive, or write to a case ledger, including the CASE_MANAGER's own acts. A semantic type means the same thing wherever the activity appears.
- Every ledger payload is a valid AS2 activity. No payload uses an activity type that AS2 does not define.
- An entry's
eventTypeis the semantic type the registry assigns to itspayloadSnapshot. It is written at commit time and kept so that replay can route on it, but it is never chosen independently of the payload. Where two meanings share one shape, the distinguishing fact goes into the payload, so a pattern can see it, or the two meanings are one semantic type. - A CASE_MANAGER decision is an occasion, and its record is a real activity. When the clock or the CASE_MANAGER's policy decides something (an invitation's deadline passes, a late answer is honoured, an offer's deadline passes), that decision is the occasion for an act the CASE_MANAGER performs. The act is an activity the CASE_MANAGER builds with a factory, commits from its sealed blob and delivers like any of its acts; it is never a ledger entry shaped to look like an activity, and never an activity attributed to a participant that sent nothing.
- An expired embargo invitation is the CASE_MANAGER's
Update(EmbargoConsent)carrying the invitee'sTIMED_OUTrow. - An honoured late
Acceptof an embargo invitation is ruled by ADR-0152: the receivedAcceptitself is committed, asaccept_invite_to_embargo_on_case, carrying the row it states as an on-timeAcceptdoes, and noUpdate(EmbargoConsent)follows it. - An expired ownership-transfer
Offeris ruled by ADR-0152: its expiry is an observation, so nothing is ledgered for it, and a lateAcceptis refused from theOffer's ownendTime. No separate act entry precedes theUpdate(EmbargoConsent): the row's state already tells an expiry from a refusal, and a second entry would record an act nobody sent. The syntheticExpiretype is retired. - Recognizing a semantic type does not imply a handler for it. A receiver maps a received activity to a use case by its semantic type; when none is registered, the activity is not processed ("no use case found"). That outcome says the receiver does not handle the message, not that the message is invalid. Permission to send a message is a separate question, owned by the use cases that handle it (ADR-0115), never by the inbox.
Consequences
- Good, because a ledger entry's meaning has one source, and a label that disagrees with its payload fails a test instead of reaching a replica.
- Good, because the registry can be exported as data, so the demo UI and the report tool share one vocabulary and its phrasing.
- Good, because a semantic type the registry recognizes but no receiver handles is visible as a gap, which tells implementers which handlers are still missing.
- Neutral, because
eventTypestays on the entry as a stored copy, so the stored ledger format does not change. - Bad, because every ledger-only act needs a registry entry and a pattern, and a shape that today carries two meanings needs a payload change or a merge.
- Bad, because a received activity whose shape is only ever ledgered, such as a
Createof a case record (ADR-0137), is now recognized and then left unhandled, where before it was unrecognized.
Validation
- A test runs every registered demo scenario and asserts that, for every ledger entry written, the registry's matcher returns the entry's
eventType(#4724). - A test asserts that no ledger payload uses a non-AS2 activity type.
- The exported registry table that the demo UI reads is drift-tested against the registry (#4725).
Pros and Cons of the Options
The payload is the only source
- Good, because no stored label can ever disagree with the payload.
- Bad, because every replica would run the pattern matcher on every entry it replays, where today it compares one string.
- Bad, because it changes the stored ledger format and the replication wire shape.
An independent label from a closed list
- Good, because it is the smallest change: declare the existing strings as one enumeration and export it.
- Bad, because the label and the payload remain two descriptions with nothing tying them together.
- Bad, because ledger-only acts keep a naming scheme of their own, outside the vocabulary the registry documents.
Valid AS2 payload, with eventType equal to the registry's label
- Good, because the ledger and the registry describe one vocabulary, and the stored label is checked rather than trusted.
- Good, because replay keeps routing on one string.
- Bad, because the equality is a test-time guarantee over the scenarios the tests run, not a construction-time one.
More Information
- Decided in the 2026-10-15 demo UI planning session, which audited the merged demo UI (PR #3754).
- The replacement for the non-AS2 payload type and the rulings for the honoured late
Acceptand the two expiries (item 4) were decided on #4723 and built in #3743. The shapes for the other ledger-only acts, and the rulings for the other shared shapes, are still decided in #4723. - Whether to rename
eventTypeto match the glossary term is open in #4734. - Glossary: Semantic type in
CONTEXT.mdand the glossary.