The Formal Message Set and the AS2 Wire Vocabulary Are Deliberately Different Shapes; the Mapping Is the Reconciling Artifact
Context and Problem Statement
Vultron has two message vocabularies. The formal protocol
(Message Types) defines 28 shorthand
symbols partitioned by which state machine a message belongs to: RM, EM, CS, and
General. The implementation has a SEMANTIC_REGISTRY of AS2 wire activities,
each pairing an ActivityPattern with a MessageSemantics value, an event
class, a use case, and a rendering phrase.
These two sets do not correspond one-to-one, and until now nothing said so plainly. The prevailing implicit assumption — that a shorthand and a wire activity are two names for one thing — is wrong in three distinct ways, and it has already produced a normative defect and a documented-but-never-built wire format.
Concretely, at the time of this decision:
- Several shorthands collapse onto one wire activity.
CVandCFboth rideas_ParticipantStatus.vf_state;CP,CX, andCAall rideas_CaseStatus.pxa_state;EV,EJ, andECare sent asEP,ER, andEA. The discriminator is a payload field, not the activity type. - Several shorthands expand into many wire activities.
EPis realized by four;GIcovers the whole note lifecycle and the actor-suggestion handshake. TheCreate(X)+Add(X → Y)split is itself an expansion with no formal counterpart at all. - Roughly half the registry has no formal counterpart: case lifecycle, participant roster, invitations, role delegation, ownership transfer, case proposal, and ledger replication.
Two consequences of the unstated assumption were already live defects.
MSM-03-001 through MSM-03-003 asserted at MUST that CV, CF, and CD
dispatch as ADD_CASE_STATUS_TO_CASE and that as_CaseStatus carries
vendor_aware / fix_ready / fix_deployed payload flags. No such fields exist;
the V/F/D dimensions are participant-scoped and live on as_ParticipantStatus
per ADR-0075. And
howto/activitypub/activities/error.md documents a four-way wire fault taxonomy
(RmError, EmError, CsError, GmError, discriminated by as:inReplyTo)
that exists in no ontology file and no code, while
ActivityPattern.in_reply_to_ is used by no registered pattern.
The question this ADR settles: is the divergence a defect to be driven to zero, or a deliberate property to be documented?
Decision Drivers
- The formal message set is normative and versioned with the protocol specification; it must remain stable for interoperability and conformance claims.
- The AS2 wire vocabulary must remain idiomatic ActivityStreams, reusing existing verbs rather than minting Vultron-specific ones.
- Implementers need one place that answers "what is this message, when is it sent, what does it look like on the wire, and where do I handle it."
- Silent divergence between the two sets has already produced a wrong
MUSTrequirement and a phantom wire format. Whatever is chosen must make divergence visible rather than merely absent. - Hand-maintained correspondence tables in this project have a demonstrated failure record.
Considered Options
- Treat the divergence as deliberate and make the mapping a first-class, rendered artifact
- Force the wire vocabulary to mirror the formal message set one-to-one
- Retire the formal message set and treat the wire vocabulary as the only message definition
- Leave both sets as they are and document neither the mapping nor the divergence
Decision Outcome
Chosen option: treat the divergence as deliberate and make the mapping a first-class, rendered artifact, because the two sets are answering different questions and flattening either one onto the other destroys information that the protocol needs.
The formal set partitions by which state machine a message concerns, which is the right axis for reasoning about protocol conformance and for the transition tables. The wire vocabulary partitions by what ActivityStreams verb and object faithfully express the act, which is the right axis for interoperability with ActivityPub tooling. Neither axis is derivable from the other, so the mapping is irreducible and belongs in the specification rather than in a reader's head.
Three commitments follow.
1. The mapping is normative and bidirectional.
specs/message-semantics-mapping.yaml (MSM) is the authoritative bridge. It
records collapses and expansions explicitly, and every row carries a status
saying which kind of relationship it is. MSM-04 adds the General shorthands that
were previously absent from the corpus.
2. Fault and acknowledgement mechanisms are partitioned on their own axes, and that is recorded rather than treated as a gap. MSM-05 specifies both:
- Faults are partitioned by failure mode, not by state machine:
Create(ProcessingFault)for received-but-not-understood,as:Rejectfor understood-but-declined, andCreate(Note)/Add(Note → Case)for a condition needing narrative explanation. Failure mode is actionable to a receiver in a way that originating state machine is not, which is why the formalRE/EE/CE/GEpartition was not reproduced. - Acknowledgement of ledger-replicated state is cumulative and implicit via
hash-chain continuity: a matching
prev_log_hashis the acknowledgement, and a mismatch triggersReject(CaseLedgerEntry)and gap-fill replay. Per-messageEK/CK/GKwould be redundant.RKsurvives as a real wire activity because report submission is not ledger-replicated.
Both were previously recorded in MSM only as absences ("no AS2 wire representation"), which invited the reading that the behaviour was unimplemented. It is implemented; it is shaped differently.
3. Reference material is rendered from the registries, never hand-written.
MSM-06 requires the consolidated per-message-type pages under
docs/reference/messages/ to render their mapping tables at build time from the
MSM spec registry joined against SEMANTIC_REGISTRY, following the existing
docs/reference/specs/*.md pattern, and requires a ratchet asserting every
registry entry reaches its designated primary page. None of that machinery exists
yet — this ADR decides the approach; the pages and the ratchet are built by the
issues listed under "More Information".
Consequences
- Good, because the collapses become documented facts rather than traps. An
implementer reading the
CSpage learns that six shorthands share two wire activities and which payload field discriminates them. - Good, because the wire vocabulary can evolve toward better ActivityStreams idiom without being pinned to the formal set's partitioning, and the formal set can stay stable for conformance without being dragged by prototype churn.
- Good, because divergence becomes detectable once MSM-06-002's ratchet is built: a new registry entry with no MSM row, or an MSM row with no registry entry, will fail a check instead of quietly becoming a stale table.
- Good, because it caught the MSM-03 defect: the mapping could not be rendered correctly from an incorrect spec, which forced the error into the open.
- Bad, because there are now two vocabularies a contributor must learn, and the mapping is a third artifact to keep correct.
- Bad, because the mapping is genuinely many-to-many, so no simple lookup table suffices and the reference pages must carry per-row status annotations to be honest.
- Neutral, because the divergences MSM-05 records are now visible enough to
reopen deliberately. Whether
messages.mdshould grow new message types for the currently-unmapped wire activities, or restate its error and acknowledgement partitioning, is a normative-specification question this ADR makes askable without answering.
Validation
In place as of this decision:
spec-lintvalidates MSM structure and cross-references, including that every cited ADR and spec ID resolves.test/test_message_semantics_mapping.pyasserts the MSM-03, MSM-04 and MSM-05 claims against the live registry: thatCV/CF/CDdispatch asADD_PARTICIPANT_STATUS_TO_PARTICIPANTand thatas_CaseStatuscarries no V/F/D field, thatGIexpands across the note lifecycle and the actor-suggestion exchange, that no dispatch value is named forRE/EE/CE/GEorEK/CK/GK, that the three fault mechanisms are all registered, that the ledger NAK path exists, and thatas:Rejectis overloaded across error and ordinary refusal.
Deferred to the implementation issues, and therefore not yet validating anything:
- MSM-06-002's ratchet, which will assert every
SEMANTIC_REGISTRYentry reaches its designated primary reference page, withunknownandunknown_unresolvable_objectas the declared exemptions (#2998). - The build-time rendering that will make table-versus-code drift structurally impossible rather than merely tested for (#2998). Until that lands, the divergence this ADR describes is documented but unguarded.
Completed since this decision:
docs/howto/activitypub/activities/error.mdno longer depicts the phantom fault taxonomy. It describes the MSM-05-001 failure-mode trichotomy, andActivityPattern.in_reply_to_has been removed from the model (#3214). The design rationale for the failure-mode partition is published as Activity Vocabulary Design.
Pros and Cons of the Options
Treat the divergence as deliberate and make the mapping a first-class artifact
- Good, because each vocabulary stays optimal for its own purpose.
- Good, because it is the only option that can express a many-to-many relationship without losing information.
- Good, because it makes the unmapped regions visible and therefore schedulable.
- Bad, because it institutionalizes a translation layer that must be maintained.
Force the wire vocabulary to mirror the formal message set one-to-one
- Good, because a single vocabulary is simpler to learn.
- Bad, because it is not achievable without damage in either direction. Splitting
Add(CaseStatus)intoCP/CX/CAvariants would mint three Vultron-specific activities where ActivityStreams already has one adequate verb, violating the design goal inhowto/activitypub/activities/index.mdof not creating activity types when an existing type suffices. - Bad, because the reverse direction would require the formal set to absorb
roughly 28 case and roster mechanics it deliberately does not model as
messages, plus the
Create/Addobject-lifecycle split, which is a wire concern with no protocol meaning. - Bad, because it cannot represent the V/F/D scope asymmetry at all: a
case-scoped
CVmessage cannot say which vendor became aware.
Retire the formal message set and treat the wire vocabulary as the only message definition
- Good, because it removes the mapping entirely.
- Bad, because the formal set is the basis of the transition tables, the behavioral conformance specs, and any conformance claim; the wire vocabulary is a prototype artifact that changes far more often.
- Bad, because it would make protocol conformance depend on the current shape of one implementation's dispatch registry.
Leave both sets as they are and document neither
- Bad, because this is the status quo that produced the MSM-03 defect and the
phantom
RmErrortaxonomy. The divergence exists whether or not it is written down; leaving it unwritten only removes the opportunity to notice when it is wrong.
More Information
Design rationale, the full collapse and expansion inventory, the page
architecture, and the MSM-03 post-mortem are in notes/message-type-reference.md
(repository-only; not published to the docs site).
Which name the reader gets (added by #3456)
This decision says the two vocabularies are different shapes. A corollary it did
not draw is which shape reader-facing documentation should use, and answering it
wrongly is easy: there are three names for every activity — the formal message
(RV, "Report Valid"), the prototype class (RmValidateReport), and the wire form
(Accept(Offer(VulnerabilityReport))) — and treating the choice as a two-way one
between the last two loses the protocol layer entirely. Accept(Offer(…)),
TentativeReject(Offer(…)) and Reject(Offer(…)) are valid, invalid and
closed, and nothing in the wire form says which, so the mapping this ADR makes
first-class is exactly what a reader cannot reconstruct unaided.
The resolution, specified as DF-09-010: reader-facing docs/ pair the formal
message name and code with the wire form, and drop the prototype class name, which
survives only as a non-rendered identifier. The rejected alternative — substituting
the wire form for the protocol name — was attempted in #3458 and reverted.
Related decisions: ADR-0075 (the V/F/D split that MSM-03 predated), ADR-0036 (dimension objects), ADR-0039 (role-delegation wire format), ADR-0023 (case proposal), and ADR-0077 (ledger replication).
Source: IDEA-605.
Generated spec requirements: message-semantics-mapping.yaml MSM-04 through
MSM-06, and the corrections to MSM-01-007, MSM-02-008, MSM-02-009, MSM-03-001
through MSM-03-003, MSM-03-007, and MSM-03-008. Also
diataxis-requirements.yaml DF-09-010 (#3456), the documentation-naming
corollary above.