Skip to content
Reference

Case Ledger and Replication

This page is normative

This page is considered a core part of the Vultron Protocol. This is a normative section of the documentation.

The whole specification on one page

The specification is also published as a single page that holds every section and annex in order, for printing or for searching the whole text at once.

This page holds section 14.5 of the Vultron Protocol Specification, part of its Requirements Catalog. Each row is one requirement, and the rest of the documentation links a requirement to its row here.

14.5 Case Ledger and Replication

The requirements on the case ledger and its copies: how the CASE_MANAGER records case events, how the ledger is synchronized, how a participant keeps its case replica, and the trust a new replica starts from.

Case Ledger Processing

Requirements for participant assertions, CASE_MANAGER canonicalization, local case audit ledgering, recorded-history projection, and replication-facing CaseLedgerEntry objects. Covers the commit authorization model, per-case genesis hash derivation, the canonical entry criteria that separate the case ledger from the process log, the CASE_MANAGER inbox routing path that enables ledger entry authorship, and the causal-ordering model that makes log_index order the authoritative causal sequence.

Assertion Intake

ID Priority Requirement Related
CLP-01-001 MUST Non-CASE_MANAGER activities that participate in a report, proto-case, or case workflow MUST be treated as participant assertions awaiting CASE_MANAGER processing

A participant can only claim that it did something. Only the CASE_MANAGER can make that claim part of the shared case history. If a receiver treats a participant's activity as settled fact, two receivers can disagree about the same case. Treating it as an assertion keeps one place where the case history is decided.
CLP-01-002 MUST NOT The protocol MUST NOT require a separate "asserted mode" marker on ordinary participant activities for case/proto-case flows

The sender's role and the path of the message already show that a participant's activity is an assertion. A separate marker adds a field that every sender must set and every receiver must check, and a missing marker would then change the meaning of an ordinary message.
CLP-01-003 MUST The CASE_MANAGER MUST be the only actor that emits canonical case-history entries used by replicas

Replicas agree only when they copy one history. If more than one actor can write entries that replicas apply, two actors can write different entries at the same position, and the replicas fork. One writer gives one order with no consensus step.
CLP-01-004 MUST NOT Participant replicas MUST NOT project shared case state directly from peer assertions

A peer's activity is an assertion that the CASE_MANAGER has not yet admitted. A replica that applies it directly can hold state that the ledger never records, so it drifts from every other replica. Replicas stay equal only when each change comes from a ledger entry.

Canonical CaseLedgerEntry

ID Priority Requirement Related
CLP-02-001 MUST For each case-resolvable assertion the CASE_MANAGER admits at the case layer, it MUST append a canonical CaseLedgerEntry

The ledger is the case history that every replica copies. An admitted assertion with no entry changes the case at the CASE_MANAGER only, and no replica can learn of it. One entry for each admitted assertion keeps the replicas complete.
CLP-02-002 MUST CaseLedgerEntry MUST be a canonical content object distinct from any transport envelope used to replicate it

The transport envelope changes from one delivery to the next. It has a different id, recipient and time each time. The hash chain must cover content that is the same for every holder. If the entry and its envelope are one object, a redelivery changes the content that the hash covers.
CLP-02-003 MUST CaseLedgerEntry MUST include the asserted activity payload as its payloadSnapshot, containing all fields required for deterministic replay: the received body for an activity the CASE_MANAGER received, or the body it delivered for one it emits (CLP-07-011, VM-08-003).

A replica rebuilds the case only from what the entry carries. If the entry holds a reference or a summary in place of the activity, a replica must find the rest somewhere else, and two replicas can find different things. The full activity lets every replica replay the same change.
CLP-02-004 MUST Every CaseLedgerEntry records a canonical admitted assertion; a CaseLedgerEntry MUST NOT carry a disposition, a reason code or a reason detail, because the ledger contains only admitted entries (see CLP-04-007).

A refusal never becomes an entry (CLP-04-007), so a disposition field could only ever hold one value. A field like this invites code to write refused entries and readers to filter them. Without it, every entry is a change that every replica must apply.
CLP-02-005 MUST When a precondition guard at the CASE_MANAGER refuses an incoming assertion, the refusal MUST be emitted as a Reject protocol activity directed at the asserting participant (CLP-05-001); a refusal MUST NOT be written to the canonical case ledger. A refusal by the sender guard (HP-01-006) and traffic after the case has fully closed (CM-23-013) are answered by no Reject.

The refused participant must learn that it was refused and why, so it can stop waiting for its entry. A refusal changes nothing in the case, and a ledgered refusal would put a non-event into every replica's hash chain. A sender that is not entitled to make the assertion is owed no answer (HP-01-006, ADR-0141). A closed case's CASE_MANAGER acts like an exited process, and a Reject of a late activity could read as a protocol decision (CM-23-013, ADR-0131).
CLP-02-006 MUST CaseLedgerEntry MUST include a log_index field corresponding to SYNC-01-002.

A replica uses the index to find its position in the ledger and to see a missing entry. Without an index on each entry, a replica cannot tell a gap from the end of the ledger.
CLP-02-008 MUST The log_index field of a CaseLedgerEntry MUST be assigned by the CASE_MANAGER alone, when it commits the entry.

Centralising log_index assignment in the single authoritative write path prevents gaps, duplicates, and out-of-order index values that would break hash-chain integrity.
CLP-02-009 MUST NOT The log_index field of a CaseLedgerEntry MUST NOT be assigned by external parties.

Allowing external parties to set log_index would enable index spoofing, enabling forked or replayed ledger entries that corrupt the canonical append-only history.
CLP-02-007 MAY CaseLedgerEntry MAY include an optional term field reserved for Raft cluster use.

In Raft, each entry carries the term of the leader that wrote it, and nodes compare terms to find entries from an old leader. A reserved field lets a cluster add that value without a change to the entry format that single-node deployments also read.
CLP-02-012 MUST "Accept" and "Reject" MUST be used only for the AS2 activities. The CASE_MANAGER's verdict on whether an assertion enters the ledger is admitted or refused, and the rules that keep refusals off the ledger (CLP-02-005, CLP-04-007, CLP-05-002) govern that verdict, not the Reject activity. A participant's Reject or TentativeReject answering a request (an Offer or an Invite) is a case event: when the CASE_MANAGER admits it, it MUST commit a CaseLedgerEntry for it under CLP-02-001, as it does for the matching Accept.

The same word cannot name both the ledger verdict and an AS2 activity. If it does, a reader takes "a refusal is not ledgered" to mean "a Reject is not ledgered". A participant's decline closes a request and reply pair, and the ledger must record that (CLP-11-001). If the decline has no entry, every replica keeps the request open.
Refines: CLP-02-001
Refines: CLP-05-002
CLP-02-013 MUST Every activity the CASE_MANAGER admits at the case layer is a case event and MUST be ledgered (CLP-02-001), except the following, which are not case events: activities processed before a case exists (report submission, case proposal intake); RK, which answers the reporter's Offer outside the case and is never sent to the CASE_MANAGER (RMB-08-003); a replica's receipt of Announce(CaseLedgerEntry), which applies an entry rather than asserting one; activities about the ledger itself, namely the CASE_MANAGER's refusal notice (CLP-05-001) and a replica's Reject(CaseLedgerEntry) asking for redelivery; the activity CLP-10-013 deliberately leaves unledgered, namely a replica's receipt of Announce(VulnerabilityCase) (ADR-0111); an activity only the CASE_MANAGER sends, received by a participant (the bootstrap Create(VulnerabilityCase), its Announce(EmbargoEvent) notice, its Accept or Reject answering a recommender's Offer(Actor)): a participant never commits (CLP-01-003), so such an activity is ledgered, if it is a case event, where the CASE_MANAGER sends it, and the bootstrap Create(VulnerabilityCase) is the case's genesis entry itself (CLP-07-011); a Create(ProcessingFault) NACK (ADR-0080), which reports on message handling rather than changing the case; a late Accept of an embargo invitation that arrives when the case has no embargo for it to bind (EMB-17-004), which changes nothing at any receiver (ADR-0152); and a message whose meaning the receiver does not recognize or whose object cannot be resolved. The list is closed: a new kind of activity that carries no entry MUST be added here first.

A closed list makes each activity with no entry a recorded decision, not the choice of one handler. A coverage test then has a fixed set of grounds for an exemption. A handler that runs only at a participant has no commit to reach. An activity that changes nothing at any receiver is an observation, not an act. An entry for it would record a fact that every holder of the ledger can already derive.
Refines: CLP-02-001
CLP-02-014 MUST CaseLedgerEntry MUST carry the entry's semantic type under the wire key semanticType, and the entry hash MUST cover that value under the key semantic_type.

The field holds a semantic type, the registry's label for the entry's payload (ADR-0140), and the glossary lists "event type" as a term to avoid for it.
Refines: CLP-02-001

Audit Scope and Continuity

ID Priority Requirement Related
CLP-03-001 MUST Only assertions that can be resolved to a specific case MUST be appended to the case audit ledger

Every entry belongs to the hash chain of one case. An assertion that no case can claim has no chain to join, and an entry for it would break the rule that a ledger holds only the history of its own case.
CLP-03-002 MUST Malformed, unauthenticated, or otherwise unrouteable inbound messages that cannot be attached to a report or case MUST be handled outside the case audit ledger

A message that the receiver cannot attach to a case tells the case nothing. It is a transport or sender problem, and the receiver's own log is the place to record it. If it went into a case ledger, any sender could add noise to a case it has no part in.

Canonical Ledger and Replication

ID Priority Requirement Related
CLP-04-001 MUST The authoritative history of a case MUST be the append-only canonical ledger; every entry in it records a protocol-significant admitted assertion. The ledger contains no refused or locally-scoped entries (see CLP-04-007).

Replicas agree only when they share one source of history. If any other record could also define the case history, two holders could each follow a different source and disagree with no way to decide which is correct.
CLP-04-002 MUST Case-state reconstruction MUST use only the canonical case ledger; refused assertions are never committed to the ledger and therefore require no filter during reconstruction.

A replica that rebuilds case state from any other source can get a different answer from every other replica. Because the ledger holds no refusals, a replay needs no filter, and so no filter rule can differ between implementations.
CLP-04-003 MUST Replicated hash chains, Merkle proofs, and replay state MUST be computed over the canonical case ledger; because the ledger contains only admitted canonical entries, no disposition filtering is required during chain computation.

Two holders must compute the same hash for the same history. If each holder first had to filter entries, a different filter rule would give a different hash for the same case. A ledger with only admitted entries removes that step.
CLP-04-004 MUST CASE_MANAGER fan-out replication MUST deliver the canonical CaseLedgerEntry content, wrapped in an ActivityStreams transport envelope such as Announce

A replica must check the entry hash against the content that the CASE_MANAGER committed. If fan-out sends a rebuilt or summarized form, the replica cannot verify the chain. The envelope only carries the entry, so the entry stays the same for every recipient.
CLP-04-005 SHOULD When a participant includes log-position context with an assertion, it SHOULD use the last canonical hash or position the sender holds from the canonical case ledger

A participant and the CASE_MANAGER compare positions to find a replica that lags. A position from any other source, such as the participant's own pending records, names a point that the canonical chain does not have.
CLP-04-006 MUST The canonical recorded log MUST be the authoritative source of truth for case participant membership and case state; implementations MUST NOT derive participant membership or case state from any other source.

Membership and case state decide who receives case content and what each participant may do. If any other source could set them, two replicas at the same ledger position could disagree about who is in the case.
CLP-04-007 MUST The case ledger MUST contain only canonical admitted entries. A refusal from any processing path MUST NOT be written as a ledger entry.

Every replica applies every entry. A refusal in the ledger would make each replica tell a refusal from a change, and an implementation that fails to do this would apply a change that never happened. A refusal is recorded in the process log. A refusal by a precondition guard is also answered with a refusal notice (CLP-02-005).

Refusal Handling

What the CASE_MANAGER does with an assertion it refuses to admit to the ledger. "Refused" is the CASE_MANAGER's ledger verdict, not the AS2 Reject activity: a participant's Reject answering a request is a case event and is admitted and ledgered like any other (CLP-02-012).

ID Priority Requirement Related
CLP-05-001 MUST The refusal notice CLP-02-005 requires MUST be a Reject whose object is the refused activity, inline, with inReplyTo naming that activity's id, addressed to the asserting participant alone, and with the refusing guard's reason in summary; its id MUST be derived from the CASE_MANAGER and the refused activity. The CASE_MANAGER MUST send one for every refusal by a precondition guard (CLP-10-022), and MUST NOT send one for a refusal by the sender guard (HP-01-006) or for a repeat of an activity it has already processed. A participant that receives one MUST close the pending assertion it recorded for the refused activity and MUST NOT write case state or re-send the assertion because of it (CLP-06-005). A Reject from the case's CASE_MANAGER of an Invite or an Offer the participant sent has the same shape as a decline of that request, and the participant MUST process it as it processes the decline, which closes the pending entry: either way, the request had no effect.

A refused sender should learn that it was refused and why, and stop waiting for its assertion to be committed. One shape for every refused activity needs one emitter and one receiving use case (CS-22-001). A Reject of an Invite or an Offer already means a decline, and a decline and a refusal leave the sender in the same place, so the shapes need not differ.
CLP-05-002 MUST NOT Refusals MUST NOT be recorded as entries in the canonical case ledger; the canonical ledger contains only admitted protocol-significant assertions (see CLP-04-007).

A refusal is not a change to the case. If it is an entry, every replica must hold and skip it, and the hash chain then depends on traffic that the CASE_MANAGER did not admit.
CLP-05-003 MUST NOT Implementations MUST NOT record local-only tracking markers or duplicate-detection markers as CaseLedgerEntry objects of any kind; such markers belong in ephemeral per-actor state, not in the canonical ledger.

A duplicate marker or a local tracking marker is one actor's working state. If it is an entry, it replicates to every participant and becomes part of the hash chain. Then the chain depends on the timing of one actor's processing and not on the case history.

Pending Assertion Suppression

ID Priority Requirement Related
CLP-06-001 SHOULD Participant actors SHOULD maintain actor-local pending assertion state for recently emitted case-scoped assertions that are awaiting canonical CaseLedgerEntry confirmation

A participant waits for its own assertion to come back as an entry. If it does not record that it is waiting, its behavior can send the same assertion again before the entry arrives. A short local record stops these duplicate sends.

Canonical Entry Criteria and Ledger/Process-Log Separation

ID Priority Requirement Related
CLP-07-001 MUST A CaseLedgerEntry committed to the canonical case ledger MUST correspond to exactly one CASE_MANAGER-accepted protocol-significant assertion. A state change the CASE_MANAGER itself writes, such as the RM transitions it records for a leaving participant (CM-23-001), is such an assertion, with the CASE_MANAGER as asserter.

Each entry is one fact that every replica applies once. An entry that records no admitted assertion, or that records more than one, makes replicas apply something the CASE_MANAGER never decided, or lose part of what it decided. A change the CASE_MANAGER writes itself is also a fact, so it needs an entry like any other.
CLP-07-011 MUST The payloadSnapshot of a CaseLedgerEntry MUST be the verbatim AS2 activity that was asserted. For an activity the CASE_MANAGER received, it is the JSON body as received (VM-08-002); for one the CASE_MANAGER emits, it is the body it delivered (VM-08-003). An activity the CASE_MANAGER sends and ledgers is one activity: the case's genesis entry is the Create(VulnerabilityCase) it sends for the case, not a second activity built for the ledger. The CASE_MANAGER MUST NOT drop, re-serialize, inline, patch or merge any field of it; the metadata it adds (index, hashes, commit and receipt times, case_id) lives on the entry, outside the snapshot.

The payload snapshot is the replication substrate, and the entry hash covers it, so what it holds is what every replica treats as the event. A snapshot rebuilt from the receiver's object graph records the receiver's reconstruction, not what the sender claimed, and every rewrite is one more thing a replica must reproduce exactly to verify a hash (ADR-0107).
CLP-07-012 MUST NOT The payloadSnapshot of a CaseLedgerEntry MUST NOT be empty.

An empty payload snapshot breaks deterministic replay and makes it impossible for replica nodes to reconstruct the case state from the canonical log.
CLP-07-002 MUST Each logical state change MUST produce at most one canonical CaseLedgerEntry; the CASE_MANAGER MUST NOT commit both an admitted-participant-assertion entry and a separately-synthesized CASE_MANAGER entry for the same state change. An act and the state transitions the CASE_MANAGER records as its consequence are different facts, not the same state change. The close_case entry records a received Leave, and the status entries that follow it record the RM transitions the Leave caused (CM-23-001).

A replica applies each entry. Two entries for one change make it apply the change twice, and the ledger then shows two facts where there is one. An act and the transitions it causes are different facts, so each one gets its own entry.
CLP-07-013 MUST Every change the CASE_MANAGER makes to the case file MUST be committed as its own CaseLedgerEntry, and a change with no committed entry MUST NOT be made. The act's entry comes first. When the act's own activity states a change, such as Add(Note)[target=VulnerabilityCase] carrying the note or an invitee's Accept of the stub Invite marking its own consent and joining, that entry is the change's entry, and applying it writes only what the activity states. Each other change the act causes MUST follow as its own CASE_MANAGER-authored entry, in the order the CASE_MANAGER writes them, whose payloadSnapshot is a Create of a record the CASE_MANAGER brings into existence or an Update of a changed record, carrying the object in full (CLP-07-006). A decision the CASE_MANAGER takes from local policy or the clock that changes the case file is committed as the change it makes, its own entry, before the change is made; a decision that changes nothing, such as the observation that a deadline has passed or that a closure is complete, is not committed (ADR-0152). Moving the case's ledger position (ledgerPosition) to an entry as it is committed (SYNC-16-005) is part of applying that entry, not a change to the case file of its own, and takes no entry.

Two replicas at the same ledger position must hold the same case file, and a replica infers nothing. If a change is not in the ledger, it did not happen (ADR-0124). One act can cause several changes, such as a stub Invite and the participant record it creates for the invitee. If only the act is recorded, each replica must create the record itself, with its own ids and clock times. Full records are ledger content that the hash chain must cover (CLP-07-006).
Refines: CLP-07-002
CLP-07-014 MUST NOT Case bookkeeping, the records the CASE_MANAGER keeps alongside a case while it works it (such as who recommended whom, offer records and pending markers), is not part of the case file. It MUST NOT be committed to the case ledger, and MUST NOT be read to compute any case-file value, on the CASE_MANAGER or on a replica.

The case file is what replicates, and "same ledger position, same case file" covers only the case file. If bookkeeping is stored on the case or read to compute a case-file value, replicas at the same position can disagree on a value that no entry carried.
Refines: CLP-04-006
CLP-07-015 MUST The CASE_MANAGER MUST commit every act's entries, the case-file changes those entries carry, and every consequence the act commits, such as a re-issued stub Invite, a relayed Invite with its invitee's row, a closure step's status entry or a notice it queues, atomically, so that when any of them fails none of them is written. For a received act, the act's own entry is one of those entries. An embargo act any of whose entries will not apply MUST be refused before anything is written.

An act commits its own entry and one entry for each change it causes (CLP-07-013). If they commit one at a time, a failure part-way keeps the act and loses some of its consequences, a state no single act produces. Nothing commits the missing parts later: a repeated trigger starts a new act, and an idempotency guard stops a redelivered one. So an act succeeds or fails as a whole (ADR-0146). A received act's own entry is in the same transaction (ADR-0151), so a failed received act keeps nothing and a later delivery runs it again.
Refines: CLP-07-013
CLP-07-003 MUST The payloadSnapshot.actor field of a CaseLedgerEntry(recorded) MUST preserve the original asserting actor's identity. payloadSnapshot.actor MUST name the same actor as the inbound activity's actor, the two compared as normalized actor ids; any other mismatch indicates identity substitution. The CASE_MANAGER's identity appears on the transport envelope and in recording metadata; it MUST NOT be substituted for a participant-asserter's identity. When the CASE_MANAGER also holds a participant role in the case, their identity in payloadSnapshot.actor is the correct asserter identity and is not a substitution. A state change the CASE_MANAGER writes itself has no inbound activity to compare against. Its payloadSnapshot.actor is the CASE_MANAGER, the true asserter, and the comparison does not apply. The RM status entries that follow a close_case entry (CM-23-001) are of this kind, and the leaving participant's own act stays attributed to them in the close_case entry.

Replicas apply each entry as the act of the actor that the snapshot names. If the CASE_MANAGER puts its own identity there, the act is attributed to the wrong participant, and its effects land on the wrong record. Authority is a role, not an identity (ADR-0088), so a CASE_MANAGER that also holds a participant role can assert as that participant.
CLP-07-004 MUST NOT The canonical case ledger MUST NOT contain runtime diagnostics, demo checkpoints, synthetic verification markers, troubleshooting events, or any per-actor observability content; such content belongs in each actor's own logs.

The ledger replicates to every participant and the hash chain covers it. Diagnostics and checkpoints depend on one actor's runtime. If they enter the ledger, replicas must filter them, and the chain changes when the timing of a run changes. An actor's own log can hold any detail with no effect on the protocol.
CLP-07-006 MUST A payloadSnapshot MUST carry each nested object exactly as the asserted activity carried it. An entry the CASE_MANAGER authors carries in full every object it records. A received entry is the received body (CLP-07-011), so an object is inline where its sender inlined it, as a sender does every object its recipient cannot be expected to hold, and bare where its sender named an object the case's replicas already hold (AKM-02-002). The CASE_MANAGER MUST NOT inline, resolve or replace a reference when it records an entry, and MUST NOT refuse an entry for a bare reference anywhere but the snapshot's own object, whose type is half of the entry's signature. A replica that does not hold an object named by reference dereferences it.

A protocol rule, not this prototype's inline-object shortcut (AKM-03-003): the entry hash covers the payloadSnapshot and nothing outside it, so an object carried in full is under the hash every participant verifies, and an object named by id is only identified by it. Filling a reference in at the CASE_MANAGER would put the receiver's reconstruction in place of what was sent, and a replica could not tell the sender's claim from the fill-in (ADR-0107).
CLP-07-007 MUST payloadSnapshot context values MUST use the case URI once a case exists; report URIs MUST NOT appear as the context for ParticipantStatus or other case-scoped snapshots after the report→case promotion has occurred

The obligation falls on whoever builds the activity. A received snapshot is the body as sent (CLP-07-011), so the CASE_MANAGER neither writes a context in nor rewrites one; the commit boundary refuses a context naming another case, and the entry names its case on the envelope (case_id), so a snapshot that names no context, such as the Offer that opened the case, is still that case's entry.

Per-Case Genesis Hash Derivation and Origin Binding

ID Priority Requirement Related
CLP-08-001 MUST Each case ledger MUST use a per-case genesis hash as the predecessor hash for its first entry; a globally-shared constant MUST NOT be used as the genesis predecessor hash across all cases

A shared global constant provides no cryptographic origin-binding: a ledger with all entries stripped is indistinguishable from a legitimately new empty ledger, and the well-known constant enables sync DoS amplification by allowing full-replay requests without any case-specific knowledge
CLP-08-002 MUST The per-case genesis hash MUST be derived as SHA-256(case_id + "|" + created_at + "|" + owner_actor_id), where case_id is the canonical case URI, created_at is the case creation UTC timestamp (the case's published) in ISO 8601 format, and owner_actor_id is the actor id of the Case Owner, the case's attributedTo when the case is created. The same derivation applies on every creation path, including a case the CASE_MANAGER creates from the owner's CaseProposal.

Including all three values cryptographically anchors the chain's starting point to the specific case's origin identity and timestamp, allowing any party who knows the case metadata to independently verify the ledger's genesis. The owner is the anchor because the CaseActor that may create the case is the owner's delegated proxy, so a case created by its owner and one created for the owner from a CaseProposal (CP-09-001, CM-02-008) start from the same identity. The decision is recorded in ADR-0117.
CLP-08-003 MUST The case-specific genesis hash MUST be stored on VulnerabilityCase as a required field computed at case creation time, so that receivers can verify that a ledger starts correctly for this case

A receiver checks the first entry of a ledger against the genesis hash. If the case does not carry the hash, each receiver must compute it again from fields it may not hold in the same form, and two receivers can get different values. A field set at creation gives every receiver the same value.
CLP-08-004 MUST A case ledger that holds no recorded entries MUST report the case-specific genesis hash as its tail hash; it MUST NOT report a globally-shared constant sentinel.

A shared constant is the same for every case. An empty ledger that reports it cannot show which case it belongs to, and the empty ledger of one case looks the same as the empty ledger of any other case. The per-case hash makes an empty ledger specific to its case.
CLP-08-005 MUST Sync replay and backfill requests that supply a from_hash value MUST use the case-specific genesis hash (or the hash of a known recorded entry) as the starting point; implementations MUST treat any unrecognized from_hash value as a request to replay from the beginning of the recorded projection

A starting hash tells the CASE_MANAGER where the requester's copy ends. Only the genesis hash and the hashes of real entries name a position in the chain. A value that names no known position cannot be trusted, so a full replay from the start is the only safe answer.
CLP-08-006 MUST The case URI used for origin-binding MUST contain a cryptographically random UUID component in its path segment to satisfy the non-spoofability guarantee of the per-case genesis hash (e.g., https://example.org/cases/<uuid>).

A cryptographically random path segment makes the genesis hash unique and unguessable per case, so a ledger with all entries stripped is distinguishable from a legitimately new empty ledger.
CLP-08-007 MUST NOT Implementations MUST NOT use sequentially predictable or human-chosen path segments in the case URI when origin-binding integrity is required.

Predictable or human-chosen path segments allow an attacker to pre-compute a valid genesis hash and construct a forged chain. See notes/case-ledger-authority.md for future improvement paths (secret nonce, CASE_MANAGER keypairs).

CASE_MANAGER Inbox Routing for Canonical Ledger Entry Authorship

Requirements for the end-to-end routing path that delivers protocol-significant activities to the CASE_MANAGER's inbox so the CASE_MANAGER can author canonical ledger entries. Covers outbound addressing rules, the rule that the CASE_MANAGER commits its own emissions in the emitting tree rather than mailing itself a copy (ADR-0109), and the pre-flight guard that prevents non-CASE_MANAGER invocations from committing ledger entries.

ID Priority Requirement Related
CLP-10-001 MUST Every protocol-significant act a participant originates MUST send at least one activity to the case's CASE_MANAGER, so that the CASE_MANAGER receives the activity and can author a canonical ledger entry. This requirement applies to ALL participants — the CASE_MANAGER is a participant with extra duties; it is not excluded from CLP-10-001. When the CASE_MANAGER itself originates a protocol-significant activity (e.g., inviting an actor to the case), it MUST commit the canonical entry itself as it sends the activity, and MUST NOT address a copy of the activity to itself in any addressing field. A participant reaches the CASE_MANAGER only by sending it the participant's own activity, never by acting as the CASE_MANAGER (CM-24-004, ADR-0109).

The CASE_MANAGER is the only writer of the ledger, and it writes only what it receives or sends. An act that sends nothing to the CASE_MANAGER has no entry, and no replica learns of it. When the CASE_MANAGER originates an act, it commits the entry in the tree that sends the activity. A copy to itself would come back through its inbox and could be committed a second time. Only the container that hosts the CASE_MANAGER may act as it (ADR-0109).
CLP-10-004 MUST NOT An Announce(CaseLedgerEntry) replication envelope MUST NOT itself be recorded as the payloadSnapshot of a canonical ledger entry; it is the envelope that carries canonical entries to participants, not a ledger event.

The Announce(CaseLedgerEntry) envelope only delivers entries. An entry whose payload is the envelope would itself need an envelope to deliver it. Each delivery would then add one more entry with no end, and the hash chain would change with every delivery.
CLP-10-015 MUST When a receiver must identify a subject that an incoming message names (the invitee of an embargo invitation, the actor whose consent an accept or reject records, or the target of any participant-scoped action), it MUST take that identity from the received activity, never from its own identity as the receiving actor. The two coincide in the ordinary single-recipient case and diverge whenever a message is relayed on a participant's behalf.

The receiving actor and the subject of a message are the same in the usual single-recipient case. They differ when a message is relayed on a participant's behalf, sent from the command line, or replayed from the log. If the receiver uses its own identity as the subject, it records consent for the wrong participant, or sends an action to the wrong one. A conforming peer then sees a change on an actor that the message never named.
Refines: CLP-10-005
CLP-10-016 MUST Subject-identity resolution (CLP-10-015) MUST NOT select a subject by its position in the to: list.

The order of the to: list carries no meaning. A pick by position resolves a message with several recipients to the same subject in every replica. Each recipient after the first would then apply the message to the record of another participant.
Refines: CLP-10-015
Depends on: EP-09-010

Protocol Request/Reply State Detection

Requirements for querying whether a protocol-level request/reply pair (e.g., Offer(CaseParticipant) → Accept/Reject) is still open or has been closed. These rules establish the canonical pattern for duplicate-emission suppression and CASE_MANAGER decision-gate checks.

ID Priority Requirement Related
CLP-11-001 MUST Whether a protocol request/reply pair (e.g., Offer(CaseParticipant) awaiting Accept or Reject) has been authorized MUST be determined from the case ledger, and MUST NOT be determined from the actor's outbox.

The outbox is a delivery queue: entries are removed when the activity is sent and MUST NOT be used to infer protocol state. The case ledger is the canonical append-only protocol history and survives actor restarts. Any code that reads the outbox to determine whether a request is still in-flight is architecturally incorrect and MUST be replaced.

CaseLedger Causal Ordering — CASE_MANAGER Timestamp Invariants

Normative requirements establishing that log_index order is the canonical causal order from the CASE_MANAGER's perspective, and the temporal invariants that conformance tests can verify against any ledger. See ADR-0079 for the decision record and rationale.

ID Priority Requirement Related
CLP-14-001 MUST A case ledger MUST record entries in causal order from the CASE_MANAGER's observation perspective: if the CASE_MANAGER observes event A before event B, then log_index(A) MUST be less than log_index(B). The log_index sequence is the authoritative causal order; timestamp order is supporting evidence only.

The CASE_MANAGER is the single authoritative writer (CLP-01-003, ADR-0041). A single-writer regime produces a total order by construction, so no distributed consensus mechanism is needed. ADR-0079 formalises this design intent as a normative requirement; DEMOMA-22-005 depends on it.
Refines: CLP-01-003
Refines: SYNC-01-002
CLP-14-002 MUST The published timestamp on the CaseLedgerEntry envelope — the CASE_MANAGER's commit stamp, as distinct from the claimed payloadSnapshot.published — MUST be non-null. The CASE_MANAGER MUST stamp each entry with its own authoritative clock at commit time.

A null entry timestamp makes the ordering checks impossible, and it breaks any consumer that orders entries by time. An entry carries two timestamps. A rule for the envelope stamp that is applied to the claimed stamp, or the reverse, gives a check that never fires or that refuses valid entries, so the rule names its field.
CLP-14-003 MUST The envelope published timestamps (the CASE_MANAGER's commit stamps) on consecutive recorded entries MUST be monotonically non-decreasing in log_index order. An entry with a higher log_index MUST NOT carry an envelope published timestamp earlier than the entry with the immediately preceding log_index. This requirement does NOT apply to the claimed payloadSnapshot.published, whose cross-entry ordering is constrained only per participant by CLP-15-003.

A timestamp that goes backward contradicts the causal order. If entry N has a later log index than entry N-1 but an earlier stamp, observers cannot use the stamps to support the order that the CASE_MANAGER recorded. The rule covers only the envelope stamp, because one writer stamps every entry from one clock. On the claimed stamps it would compare the clocks of two participants, which ADR-0079 rejects as option C, and it would refuse a valid assertion when two clocks disagree.
Refines: CLP-14-001
CLP-14-004 MUST All entries in a single case ledger MUST share the same case_id. An entry whose case_id does not match the ledger's case_id MUST be treated as malformed and rejected.

A ledger mixing entries from multiple cases makes causal comparisons meaningless and breaks hash-chain verification (which is scoped per case).
CLP-14-005 MUST log_index values MUST be unique within a single case ledger. No two entries for the same case_id may share a log_index value.

Duplicate indices would make it impossible to identify which entry is causal-order position N, breaking any test or consumer that relies on log_index to determine ordering.
Refines: SYNC-01-002
CLP-14-006 MUST Every CaseLedgerEntry's envelope published timestamp MUST be on or after the parent VulnerabilityCase's own published timestamp. A ledger entry MUST NOT be stamped with a time that precedes the case's creation. The CASE_MANAGER MUST additionally reject a recorded assertion whose claimed payloadSnapshot.published precedes the parent case's published timestamp by more than the configured clock-skew tolerance (CLP-14-009).

The CASE_MANAGER is the single writer and creates the case before committing any entries. An entry predating the case indicates a clock error or commit-path bug; treating it as a protocol violation catches the defect at the boundary. The envelope half holds by construction and is verified by the conformance harness. The claimed half needs a tolerance because the participant's clock is not the CASE_MANAGER's — a zero-tolerance comparison across two clocks would refuse well-formed assertions from any participant whose clock lags, which is the wall-clock coupling ADR-0079 rejected as option C.
CLP-14-007 SHOULD The CASE_MANAGER SHOULD reject a payload assertion whose payloadSnapshot.published timestamp is more than a configurable tolerance period ahead of the CASE_MANAGER's clock at the time of receipt. The default tolerance SHOULD be no more than five minutes to match common clock-skew conventions.

A payload timestamped in the future relative to the CASE_MANAGER's clock indicates a participant clock error or a malformed assertion. Accepting it silently would produce ledger entries where the payload claims to have been published after the ledger envelope was stamped — a contradiction of the observation model in ADR-0079. A small tolerance accommodates legitimate clock drift without rejecting well-formed messages from participants whose clocks are slightly ahead.
Refines: CLP-14-001
CLP-14-008 SHOULD The CASE_MANAGER SHOULD reject a payload assertion whose payloadSnapshot.published timestamp is more than a configurable staleness window in the past relative to the CASE_MANAGER's clock. The default staleness window SHOULD be seven days.

A payload with a timestamp far in the past suggests the participant is replaying old activities, potentially to reconstruct or manipulate ledger history. Rejecting stale payloads limits the window in which a replayed or delayed assertion can be inserted into the canonical ledger.
Refines: CLP-14-001
CLP-14-010 MUST The log_index values of a case ledger MUST form a gapless sequence starting at 0. The genesis entry MUST carry log_index 0, and every later entry MUST carry the log_index of its immediate predecessor plus one. The CASE_MANAGER MUST NOT skip an index when it assigns one.

Receivers find a missing entry by index arithmetic. An entry whose log index is greater than tail_index + 1 shows a forward gap (SYNC-14-001), and a replica that lags is valid only while its local prefix has no gaps. Both tests work only if the CASE_MANAGER never leaves a gap. CLP-14-001 requires only that indices increase, and CLP-14-005 requires only that they are unique. Neither excludes a sparse ledger.
Refines: SYNC-01-002

Participant Assertion Timestamp Obligations

Normative requirements on what well-behaved participants MUST do when emitting assertions to the CASE_MANAGER, so that the CASE_MANAGER's observation-order model (ADR-0079, CLP-14) yields a ledger that faithfully reflects real-world causal order within a single participant's event stream.

ID Priority Requirement Related
CLP-15-001 MUST A participant MUST emit protocol activities for a given case in causal order. If activity A causally precedes activity B in the participant's own event stream, A MUST be emitted to the CASE_MANAGER before B.

The CASE_MANAGER uses arrival order as the authoritative sequence. If a participant emits B before A despite A causing B, the CASE_MANAGER's ledger will record the wrong causal order. The participant is responsible for correct ordering; the CASE_MANAGER cannot reconstruct causality it did not observe.
CLP-15-002 MUST NOT A participant MUST NOT batch causally ordered activities and submit them to the CASE_MANAGER in arbitrary order. If events A, B, C are causally ordered (A → B → C), the participant MUST NOT submit them as C, A, B or any permutation that reverses causal dependencies.

Batched out-of-order submission produces a malformed ledger from the CASE_MANAGER's perspective. This is a protocol violation on the participant's part, not a CASE_MANAGER failure.
Refines: CLP-15-001
CLP-15-003 MUST For causally related activities within the same participant's event stream, the published timestamp on activity B MUST be greater than or equal to the published timestamp on activity A, where A causally precedes B. A participant MUST NOT assign a published timestamp to B that precedes A's published timestamp when A and B are causally ordered.

A participant that assigns earlier timestamps to later events produces a payloadSnapshot whose timestamp order contradicts the submission order. Even if the CASE_MANAGER's own entry timestamps are correct, the snapshot timestamps become evidence against the order recorded in the ledger.
Refines: CLP-15-001
CLP-15-004 SHOULD A participant SHOULD assign published timestamps that reflect the actual time the event occurred, not the time the batch was submitted or a retry was attempted.

Backdated or forward-dated timestamps degrade the forensic value of the ledger even when causal order is preserved. Timestamps that reflect actual occurrence time allow CaseActors and auditors to detect anomalies like stale replays (CLP-14-008).
CLP-15-006 MUST A participant MUST include a published timestamp on every protocol activity it emits. A receiver MUST treat an inbound activity that carries no published as malformed and reject it at message validation, and MUST NOT substitute its own clock for the missing value.

ActivityStreams makes published optional. A receiver that fills in the field cannot later tell a time the sender claimed from one it made. Each check that reads the claimed time then compares the receiver's clock with itself and never fires (CLP-14-007, CLP-14-008). A refusal at message validation keeps the difference. A receiver that creates its own outbound activity stamps its own clock, which is correct.
Refines: CLP-15-004
CLP-15-005 MUST The CASE_MANAGER's ledger is authoritative for what the CASE_MANAGER observed from its own perspective. It is NOT a guarantee of real-world causal fidelity when external participants self-report events. The CASE_MANAGER MUST record the observation-order sequence it received; it MUST NOT attempt to reconstruct participant-internal causal order it cannot verify.

Even a compliant participant's clock may drift, and a non-compliant participant may submit events in arbitrary order. The CASE_MANAGER's role is to record faithfully what arrived, in the order it arrived, while applying the timestamp sanity checks in CLP-14-007 and CLP-14-008. Attempting to reorder entries based on payload timestamps would require trusting externally-supplied data to override the CASE_MANAGER's own observation — a violation of the single-writer authority model.
Refines: CLP-01-003
CLP-15-007 MUST A receiver MUST carry the published and updated timestamps of every object nested in an inbound activity as received. A timestamp the sender omitted MUST remain absent; the receiver MUST NOT substitute its own clock for it, whether at parsing, extraction, storage or rendering. Where a decision requires a nested timestamp that is absent, the receiver MUST reject the message at message validation.

AS2 makes object timestamps optional. A value that the receiver makes is later indistinguishable from the sender's claim. For example, each receiver would derive its own genesis hash for one case, and order received statuses by arrival. An absent time harms only a decision that needs it, so that decision refuses the message early and clearly. Every other consumer accepts the absence.
Refines: CLP-15-006

Case Event Log Synchronization

Requirements for the append-only canonical recorded case ledger, replication transport, conflict handling, per-peer state tracking, and retry semantics for distributed case state synchronization across Participant Actors.

Append-Only Log

ID Priority Requirement Related
SYNC-01-001 MUST The canonical recorded case ledger MUST be append-only; log entries MUST be immutable once appended

The ledger is the record of what happened in the case. If an entry can change or go away, the hash chain breaks, replicas disagree, and nobody can audit the case history. Append-only entries let every replica apply the same history and reach the same case state.
Refines: SYNC-00-005 — Restates the immutability of SYNC-00-005 for the canonical case ledger
SYNC-01-002 MUST Each log entry MUST carry a monotonically increasing, gapless index scoped to its case

The index gives every entry one position in the case's causal order (ADR-0079). Because the index has no gaps, a receiver knows that an entry is missing when the next index it gets is too high (SYNC-14-001). A duplicate or skipped index would hide a lost entry or let two entries claim one position.
SYNC-01-003 MUST Each log entry MUST include a cryptographic hash of its own content and MUST reference the hash of its immediate predecessor, forming a forward-linked hash chain (Merkle chain style)

The content hash lets a receiver check that an entry's content matches the hash the entry states. The link to the predecessor's hash fixes the entry's place in the chain. A receiver can then detect an entry whose content does not match its hash and an entry in the wrong place.
SYNC-01-004 MUST Canonical recorded log entries MUST be written only by the CASE_MANAGER.

One writer gives one canonical order of entries. If two actors could write canonical entries, their ledgers could fork, and no rule could say which fork is the case history.
SYNC-01-005 MUST The hash-chain log entry format MUST be designed to be forward-compatible with a future Merkle Tree implementation

A Merkle tree would let a peer prove that one entry is in the ledger without sending the whole chain. If the entry format blocks that change, a migration would have to rewrite every stored ledger, which an append-only ledger forbids.

Replication Transport

ID Priority Requirement Related
SYNC-02-001 MUST Log replication between the CASE_MANAGER and Participant Actors MUST use ActivityStreams Announce activities as the transport envelope for canonical recorded log content

Vultron actors talk through ActivityStreams messages, so ledger replication uses the same inbox, outbox and parsing path as other case messages. Announce tells the recipient about an object that already exists, which is what passing on a committed entry does.
SYNC-02-002 MUST Each replication message MUST identify the sender, target recipient, the log entry hash, and the predecessor hash

The receiver needs the sender to check that the message comes from the CASE_MANAGER. It needs the two hashes to find the entry's place in its chain before it applies anything (SYNC-03-001). Naming the recipient makes each message belong to one peer, so the CASE_MANAGER can track each peer's replication state (SYNC-04-001).
SYNC-02-003 MUST Replication MUST originate from the replication leader (the actor holding the CASE_MANAGER role) and be sent individually to every active participant (CM-10-004).

Only the CASE_MANAGER holds the canonical ledger, so only it can send entries that a replica can trust. A separate message to each active participant lets the CASE_MANAGER track and repair each peer on its own (SYNC-04-001). Case content goes only to active participants, so an inert participant receives no ledger entries (CM-10-004, ADR-0114).
SYNC-02-004 MUST The object of each Announce(CaseLedgerEntry) activity MUST be the CaseLedgerEntry itself, carried inline or by reference, with every field the recipient needs to validate the hash chain (including the entry hash and its predecessor's hash).

The recipient checks the hash chain before it applies the entry (SYNC-03-001, SYNC-03-006). It can do that check only when it has every field the hashes cover. The inline form and the by-reference form give the recipient the same entry, so the protocol allows either.

Conflict Handling

ID Priority Requirement Related
SYNC-03-001 MUST A receiver MUST reject a replication message whose predecessor hash does not match the receiver's current log tail hash

An entry that does not extend the receiver's tail is in the wrong place for this replica. If the receiver applies it, the replica holds a chain that the CASE_MANAGER never wrote. The rejection tells the CASE_MANAGER where the replica's chain ends, so it can send the missing entries.
SYNC-03-002 MUST On receiving a rejection, the sender MUST retry replication starting from the entry following the last accepted hash reported by the receiver

The rejection names the last entry that the replica holds. Sending from the entry after it fills the gap with the fewest messages. Sending from any other point either skips entries the replica needs or sends entries it already has.
SYNC-03-003 MUST NOT Replication MUST be idempotent: repeated delivery of the same log entry MUST NOT produce duplicate entries in the receiver's log

The outbox retries and replays send entries again, so a receiver often gets one entry more than once. A duplicate in the log would break the gapless index and the hash chain, and its effects would apply twice (ID-02-001).
Implements: ID-02-001 — idempotency.md
SYNC-03-005 MUST The CASE_MANAGER MUST act on Reject(CaseLedgerEntry) only when the sender is an active participant in the case (CM-10-004). From any other sender it MUST refuse the message, record no replication state for the sender, and replay no entry and no case to it.

A rejection that states no ledger position, or the position of an empty chain, makes the CASE_MANAGER replay the whole ledger and send the case. Without this check, any sender could get embargoed case content just by asking. The replay cooldown limits how often a replay occurs, not who gets it.
Refines: HP-01-006
Refines: SYNC-03-002
SYNC-03-006 MUST A receiver MUST recompute the hash of a received CaseLedgerEntry from its content and compare it with the entry's stated entryHash before buffering, applying or storing the entry. An entry that states no hash, or whose content does not reproduce it, MUST be refused, and MUST NOT be buffered, applied or stored.

The predecessor check (SYNC-03-001) proves only that an entry claims the right place in the chain. It does not prove that the content matches the entry's hash. A refused entry gets no Reject(CaseLedgerEntry), because a replay would send the same entry again. The receiver's tail does not move, so the next entry does not extend it and causes a Reject in the usual way. The receiver reports the refusal with a processing fault instead (SYNC-03-007).
Refines: SYNC-01-003
Refines: SYNC-08-001
SYNC-03-007 MUST A participant that refuses a CaseLedgerEntry under SYNC-03-006 MUST send Create(ProcessingFault) with failure class LedgerEntryRefused/HashMismatch to the case's CASE_MANAGER, naming the refused Announce(CaseLedgerEntry) by reference. The fault's id MUST be derived from the refused Announce and the failure class.

The entry was received but could not be processed, which ASK-07-001 and MSM-05-001 answer with a processing fault. Without it only the replica's log shows the refusal, while the CASE_MANAGER's replay resends the same entry. A hash mismatch is not a divergence: a fresh case copy cannot repair it, because the replica still cannot store the entry, so it takes this fault and not SYNC-16-001's. Like that fault, it is replication signalling and is never ledgered (SYNC-16-002).
Refines: SYNC-03-006
Refines: ASK-07-001
SYNC-03-008 MUST When the CASE_MANAGER receives the SYNC-03-007 fault from an active participant of the case (CM-10-004), it MUST NOT send anything, re-seed the peer or record replication state because of the fault. From any other sender it MUST refuse the fault.

A hash mismatch comes from deterministic content: a fault in rendering or parsing the wire form, or a broken stored entry the send-side check (SYNC-03-009) did not see because fan-out checked its in-memory copy. Neither a replay nor a re-seed can repair it; only a code fix or an operator can. The reject-and-replay loop it leaves is already bounded by the replay cooldown (SYNC-15-003, SYNC-15-013).
Refines: SYNC-03-007
Refines: SYNC-16-002
SYNC-03-009 MUST Before the CASE_MANAGER sends a CaseLedgerEntry in Announce(CaseLedgerEntry), whether by fan-out, replay or backfill, it MUST recompute the entry's hash from its content as SYNC-03-006 does. An entry that states no hash, or whose content does not reproduce it, MUST NOT be sent and MUST be logged at ERROR with the case, the entry, its log_index and the recipients, and the CASE_MANAGER MUST NOT send any later entry of the same send. This takes precedence over SYNC-03-002, SYNC-17-004 and CM-10-006, which otherwise oblige it to send every entry in a range.

A broken entry in the CASE_MANAGER's own store is found where it starts, not after a replica refuses it (SYNC-03-006) and reports it (SYNC-03-007), and broken content never leaves the CASE_MANAGER. Holding the entry back stalls the replica at it, as sending it would. The replica's Reject loop stays bounded without the replay cooldown: a replay that starts at the broken entry, as the replica's stalled Reject makes it, sends nothing, so it records no position (SYNC-15-011), and the replica sends one Reject per later fan-out, with nothing replayed to amplify it. A later entry is not sent because the replica could only hold it as a forward gap (SYNC-14-001). The receiver's check stays, because a fault in rendering or parsing the wire form still shows up first at the receiver.
Refines: SYNC-03-006
Refines: SYNC-08-001
Constrains: SYNC-03-002
Constrains: SYNC-17-004
Constrains: CM-10-006

Per-Peer Replication State

ID Priority Requirement Related
SYNC-04-001 MUST The replication leader MUST track per-peer state including at minimum the last acknowledged log entry hash for each peer

The leader must know how far each peer has got. Without that record, it cannot tell a peer that is behind from one that is current. It also cannot decide where a replay starts or whether the replay cooldown applies (SYNC-15-008).

Retry and Backoff

ID Priority Requirement Related
SYNC-05-001 SHOULD The replication sender SHOULD implement retry with exponential backoff on delivery failure

A peer can be offline or overloaded for some time. Retries at a fixed interval add load to that peer when it can least take it. Exponential backoff keeps retries frequent for a short fault and rare for a long one.

Leadership and Ownership

ID Priority Requirement Related
SYNC-06-001 SHOULD Replication leadership SHOULD be treated as distinct from case ownership

The Case Owner decides about the case, and the replication leader writes and sends the ledger. The Case Owner can hold the CASE_MANAGER role or give it to another actor, and a multi-node CASE_MANAGER can change its leader with no change of owner. If the two concepts are one, a change of either one forces a change of the other.
SYNC-06-003 MUST In a single-node deployment, the actor holding the CASE_MANAGER role MUST be treated as the permanent replication leader with exclusive write authority for its cases; no leader-election procedure is required.

A single-node deployment is a degenerate Raft cluster of one: there is always a quorum (1/1) and no election can occur, so the CASE_MANAGER is permanently the leader without consensus overhead. This is the expected topology for the prototype phase.

Commit Discipline

ID Priority Requirement Related
SYNC-09-001 MUST An implementation MUST treat a log entry as committed only after it has been durably written to the authoritative log and its content hash has been recorded; an entry that has not met these conditions MUST NOT be applied to the case state machine.

If an actor applies an entry that is not yet durable, a crash can leave state changes with no entry that records them. The replica then holds state that its ledger cannot explain, and a replay cannot rebuild it.
SYNC-09-002 MUST A node MUST commit the CaseLedgerEntry that records a Vultron message it sends only after the message has been written to its own outbox, and MUST NOT commit it before that write. Announce(CaseLedgerEntry) is the one message sent after its commit: it delivers a committed entry, and is not an act the ledger records.

A ledger entry records a complete act, and a send is complete when the message is in the sender's outbox, which owns delivery and retries. An entry committed before the send records an intention that may never become true, and its fan-out can reach a replica before the message it records. A retry after a failed commit rebuilds the activity under its first id. The outbox does not queue it again while it is pending (OX-06-002), and the receiver drops a copy it already has (ID-02-001).

System Invariants

ID Priority Requirement Related
SYNC-08-001 MUST Append-only integrity: log entries MUST be immutable once committed and MUST be uniquely identified by their content hash

Replays and the drain find an entry by its hash. If two different entries could have the same hash, or an entry could change after commit, a replica could apply the wrong entry and still pass the chain check.
Refines: SYNC-00-005 — Restates the immutability of SYNC-00-005 as a system invariant
SYNC-08-002 MUST Deterministic projection: given an identical canonical recorded log prefix, all compliant implementations MUST derive identical state

Each replica gets its case state from the ledger. The ledger keeps replicas in step only if the same entries always give the same state, on every implementation. Any other input, such as the local clock or the arrival order, would let two replicas with the same ledger disagree.
SYNC-08-003 MUST NOT Idempotent replay: reprocessing any canonical recorded log prefix (including duplicates) MUST NOT change the resulting state

Replays, retries and restarts make an actor process some entries more than once. If a second pass changed the state, recovery itself would corrupt the case.
SYNC-08-004 MUST NOT Monotonic visibility: participants MUST NOT regress their acknowledged log position

The CASE_MANAGER uses each peer's acknowledged position to decide what to send and when to replay. A position that moves back would make it send again entries the peer already holds, and could reset the replay cooldown. An acknowledged entry stays in the peer's append-only ledger, so a lower position is never true.
SYNC-08-005 MUST Reject-on-divergence: entries that do not extend the current hash chain MUST be rejected and MUST trigger resynchronization

"Rejected" means "not applied and not persisted as the current tail"; it does NOT require discarding the entry. A forward-gap entry (a valid entry whose predecessor has not yet arrived) MAY be retained in the non-ledger holding area of SYNC-14 pending its predecessor, and MUST still trigger resynchronization (the Reject remains the backstop for genuinely lost entries). See ADR-0037 (docs/adr/0037-buffer-out-of-order-ledger-entries.md).

Catch-Up Gate Before New Actions

ID Priority Requirement Related
SYNC-10-001 MUST After actor process restart or recovery, an actor with case state in scope MUST re-establish case-ledger freshness with the CASE_MANAGER before taking new protocol-significant case actions

After a restart, an actor can have missed entries while it was down. If it acts on that old view, it can propose or answer from a case state that the missed entries have changed. Catching up first makes its next action rest on the current case history (ADR-0018).
SYNC-10-002 MUST While catch-up freshness is not established, the actor MUST block or defer new protocol-significant case actions and surface an explicit stale-or-catching-up condition

An action taken from a stale view can conflict with entries the actor has not seen, and blocking the action prevents this. A visible stale condition tells an operator or a test why the action did not run, so a stuck catch-up does not look like a silent failure.
SYNC-10-003 SHOULD Catch-up freshness checks SHOULD use the actor's last acknowledged canonical hash and replay mechanisms so that action gating is based on canonical log position, not local wall-clock assumptions

Clocks on different actors do not agree, and the time since the last message does not show whether an entry is missing. The hash of the last canonical entry is the same on every replica that holds the same prefix, so it is a reliable test of freshness.
SYNC-10-004 MUST The catch-up gate MUST require a contiguous canonical log prefix from genesis through the actor's currently acknowledged tip; any gap in that prefix MUST block protocol-significant case actions

A gap means that a committed effect is missing from the replica's case state. An action based on that state can contradict the missing entry. The hash chain shows a gap directly, so the gate can test for it without a request to the CASE_MANAGER.
SYNC-10-005 MUST NOT The catch-up gate MUST NOT require the actor's acknowledged tip to equal the CASE_MANAGER's current ledger tip; lagging behind the leader tip is allowed if the actor's own acknowledged prefix is contiguous from genesis

Fan-out is asynchronous, so a participant is often a few entries behind the CASE_MANAGER. If the gate required the latest tip, an active case would block almost every action. A contiguous prefix is enough to make the actor's own view consistent, and the later entries reach it in time.

Effects-Before-Persist Invariant

ID Priority Requirement Related
SYNC-12-001 MUST A participant MUST apply all domain effects derived from a CaseLedgerEntry (embargo teardown, participant-status update, note attachment, invite-accept) BEFORE persisting the entry to its local ledger; if any effect fails, the entry MUST NOT be persisted

A stored entry is the replica's proof that the entry's effects were applied (SYNC-12-002). If the replica stored the entry first and an effect then failed, the already-stored check (SYNC-12-003) would skip that effect on every later delivery. The replica would keep a case state that its ledger does not explain.
SYNC-12-002 MUST The presence of a CaseLedgerEntry in a participant's local ledger MUST imply that all domain effects for that entry were successfully applied; a persisted entry with unapplied effects is a protocol violation

The already-stored check (SYNC-12-003) and the drain both rely on this meaning. If a stored entry could have missing effects, nothing would ever apply them, and the replica would differ from the CASE_MANAGER permanently.
SYNC-12-003 MUST When a participant receives a CaseLedgerEntry that is already present in its local ledger, the participant MUST skip both effects and persist (treat as already-done); re-applying effects to a stored entry is not permitted because the entry's effects were applied exactly once before the initial persist

Duplicate delivery is normal under retries and replays. The effects of a stored entry were applied once, before the entry was stored (SYNC-12-001). Applying them again would repeat state changes such as a status update or an embargo teardown.

Ledger Write Ownership and Layer Boundary

SYNC-12's already-stored gate (SYNC-12-003) uses the presence of a CaseLedgerEntry in an actor's DataLayer as the sole evidence that the entry's effects were already applied. That inference is only sound if the ONLY thing that ever writes a CaseLedgerEntry to the DataLayer is the core write path that also applies the effects. If any other layer (e.g. a wire/adapter parse or rehydration helper) creates a CaseLedgerEntry record, the gate fires SUCCESS on first delivery and the effects are silently skipped. These requirements make the write-ownership boundary that SYNC-12 depends on explicit.

ID Priority Requirement Related
SYNC-13-005 MUST CaseLedgerEntry records for a case's authoritative ledger MUST be authored only by the case's CASE_MANAGER; no other actor may write authoritative case ledger entries.

The CASE_MANAGER's single authoritative write path is the enforcement mechanism for the ledger's append-only integrity and the write-ownership guarantee required by SYNC-12.
Refines: SYNC-01-004 — Restates the single-writer rule of SYNC-01-004 for CaseLedgerEntry records
SYNC-13-006 MUST Participants MUST obtain replica CaseLedgerEntry records solely from received Announce(CaseLedgerEntry) activities.

Channelling all replica writes through the receive behavior tree ensures that effects are applied in the correct order and that the already-stored gate (SYNC-12-003) is the sole idempotency mechanism.
SYNC-13-004 MUST A receiver MUST treat an inbound Announce(CaseLedgerEntry) as the announcement of a ledger entry on first delivery, without needing any stored record of the entry; whether the entry is already stored (SYNC-12-003) MUST be the only test that distinguishes first delivery from repeat delivery.

On first delivery the receiver has no stored record of the entry, so routing cannot depend on one. One test for repeat delivery keeps all implementations in agreement about when effects run. A second test could disagree with it and skip or repeat effects.

Out-of-Order Entry Buffering and Drain

Announce(CaseLedgerEntry) is delivered over a transport with no ordering guarantee, and Vultron may not be the only implementation on the wire, so a replica can receive an entry before its hash-chain predecessor. Dropping such an entry and relying on a reject/replay round-trip does not converge under adversarial reordering, because replay re-announces entries individually over the same unordered transport (they can reorder and be dropped again). These requirements make replica convergence independent of delivery order by holding the received Announce of a forward-gap entry in a non-ledger holding area and replaying it, in hash-chain order, once the entry that closes the gap is committed. What is held and replayed is the Announce that was received, never one the replica builds. Rationale: ADR-0037 (docs/adr/0037-buffer-out-of-order-ledger-entries.md), ADR-0135.

ID Priority Requirement Related
SYNC-14-001 MUST When a participant receives a valid Announce(CaseLedgerEntry) whose prev_log_hash does not match its current chain tail but whose log_index is strictly greater than tail_index + 1 (a forward gap), the participant MUST NOT permanently discard the entry; it MUST retain the Announce(CaseLedgerEntry) that carried it, as the body that arrived (VM-08-002), in a holding area pending arrival of its predecessor. It MUST NOT retain the entry apart from that Announce.

The hash chain makes it safe to hold an entry for any length of time. The replica applies a held entry only after it commits an entry whose entry_hash equals the held entry's prev_log_hash, so a fork or a rewrite cannot act as the predecessor. The replica holds the Announce and not the entry alone, so the drain applies what the replica received. An entry held alone can only reach the inbox again inside an Announce that the replica makes up, which names a sender that never sent it.
SYNC-14-002 MUST A forward-gap entry whose Announce is held MUST still trigger resynchronization (SYNC-08-005) — the participant MUST send Reject(CaseLedgerEntry) carrying its last accepted hash so the CASE_MANAGER replays entries that are genuinely lost (never delivered) rather than merely reordered.

Holding an entry covers an entry that arrives out of order. It does not cover an entry that was lost. The Reject tells the CASE_MANAGER where the replica's chain ends, so the lost entry is sent again. Without it, a replica with a lost entry would hold its later entries permanently.
SYNC-14-003 MUST After a participant commits a CaseLedgerEntry, it MUST drain the holding area: for each newly committed tail, it MUST locate the held Announce (if any) whose entry's prev_log_hash equals the new tail's entry_hash, apply that entry, and repeat, cascading until no held Announce extends the current tail.

A held entry can apply only after its predecessor. Each commit can make one held entry appliable, and that entry can make the next one appliable. If the drain stopped after one step, entries that arrived early would stay held until some other delivery came.
SYNC-14-004 MUST Draining a held entry MUST apply the entry's domain effects before persisting it (SYNC-12-001).

A drained entry becomes a stored entry like any other. The already-stored check relies on every stored entry having its effects applied, so the drain must keep the same order (SYNC-12-001).
Depends on: SYNC-12-001 — The drain MUST preserve the effects-before-persist invariant per applied entry
SYNC-14-007 MUST NOT The drain path MUST NOT re-apply effects for an entry already present in the local ledger (SYNC-12-003).

Re-applying effects for a previously committed entry would cause duplicate state transitions and violate the idempotency guarantee established by SYNC-12-003. A replayed Announce reaches the same already-stored check as any delivery (SYNC-14-008), so an entry committed between its hold and its replay is skipped, not applied again.
SYNC-14-008 MUST Applying a drained entry MUST follow the same apply-then-persist rules as in-order delivery, starting from the held Announce as in-order delivery starts from the received one.

A separate drain path can drift away from the in-order path. A drained entry could then apply its effects after it is stored, or skip a check that in-order delivery makes. One shared path keeps the effects-before-persist rule true for every entry (SYNC-12).

Genesis-Unavailable Buffer-and-Reject

A participant replica receives Announce(CaseLedgerEntry) activities from the CASE_MANAGER over an unordered transport. If the CASE_MANAGER delivers a ledger entry before the Create/Announce(VulnerabilityCase) activity has been processed by the participant (a delivery race under HTTP BackgroundTasks), the replica cannot reconstruct the per-case genesis hash (CLP-08-005) and must not silently drop the entry. Silently dropping causes the entry to be permanently lost because the CASE_MANAGER never learns that delivery failed and never replays. A pre-genesis entry is therefore held in the same non-ledger holding area used for forward gaps (SYNC-14) and drained once the case seed anchors the genesis hash, making convergence independent of whether the case or its ledger entries arrive first (issue #2186, #2180). A Reject is still sent as the loss backstop, triggering the same replay path as the hash-mismatch case (SYNC-08-005, SYNC-14-002) for an entry that is genuinely lost rather than merely early. The backstop is asked once per wait, not once per held entry, and the CASE_MANAGER answers once per need; both limits are rate limits, never stops (SYNC-15-016 to SYNC-15-025, ADR-0168). The forward-gap hold of SYNC-14 alone does not cover this window: it only holds entries for a case whose genesis anchor is already known, so a distinct pre-genesis hold and a drain triggered by the case seed are required. Rationale for the buffer-and-drain path (SYNC-15-004, SYNC-15-005): ADR-0059 (docs/adr/0059-buffer-pre-genesis-ledger-entries.md).

ID Priority Requirement Related
SYNC-15-001 MUST When a participant receives an Announce(CaseLedgerEntry) but cannot reconstruct the per-case genesis hash because it does not yet hold the VulnerabilityCase (CLP-08-005), the participant MUST tell the CASE_MANAGER by sending it a Reject(CaseLedgerEntry) with last_accepted_hash="", so that the CASE_MANAGER replays all entries from the beginning once the case is delivered, unless an earlier such Reject for the same case lets it withhold one (SYNC-15-016).

An empty last_accepted_hash means "I hold no entries; replay from genesis." The CASE_MANAGER's replay path (SYNC-03-002) replays every entry after the hash it receives. An empty string matches no entry, so it replays the full ledger. Without the Reject, the CASE_MANAGER never learns that the delivery failed, and the replica stays without the entry permanently. One Reject asks for the whole ledger, so a second one asks for nothing new (ADR-0168).
Depends on: CLP-08-005 — The missing-case failure originates from the genesis-hash requirement in CLP-08-005
Depends on: SYNC-08-005 — Reject-on-divergence applies equally to the genesis-unavailable case
SYNC-15-002 MUST NOT A participant MUST NOT persist a CaseLedgerEntry when the per-case genesis hash is unavailable.

Without the genesis hash, the replica cannot check that the entry belongs to this case's chain. A stored entry also means that its effects were applied (SYNC-13-001). Storing an unchecked entry would put an entry in the ledger that may not be canonical, and its effects would never apply.
Depends on: SYNC-12-001 — Effects-before-persist invariant applies; an entry whose genesis is unknown cannot be safely committed
SYNC-15-006 MUST NOT A CaseLedgerEntry received when the per-case genesis hash is unavailable MUST NOT be treated as committed.

Treating a pre-genesis entry as committed would populate the already-stored gate (SYNC-12-003) prematurely, causing effects to be skipped when the entry is later drained after the genesis is seeded.
SYNC-15-007 MUST NOT The domain effects of a CaseLedgerEntry received when the per-case genesis hash is unavailable MUST NOT be applied (SYNC-12-001).

Applying effects before the genesis hash is known means the effects cannot be attributed to a verified chain position; if the genesis hash later reveals the entry is a fork, already-applied effects cannot be rolled back.
SYNC-15-003 SHOULD The CASE_MANAGER SHOULD bound the rate at which it replays entries to a peer whose acknowledged replication position has not advanced.

A peer that cannot anchor its hash chain rejects every entry replayed to it. Each Reject causes a replay, and each replayed entry causes another Reject. Without a bound, this loop grows without limit and can starve the CASE_MANAGER's other work. The bound is a rate limit and not a stop, because a later Reject must still get a replay when a replayed entry is lost.
Depends on: SYNC-08-005 — Bounds the replay that the SYNC-08-005 resynchronization triggers; the reject-on-divergence contract itself is unchanged
Depends on: SYNC-03-002 — The replay-from-hash path whose rate this rule limits
Refines: SYNC-15-001 — The genesis Reject that SYNC-15-001 mandates is the highest-volume source of no-progress replays
SYNC-15-004 MUST When a participant receives a valid Announce(CaseLedgerEntry) but cannot reconstruct the per-case genesis hash because it does not yet hold the VulnerabilityCase (CLP-08-005), the participant MUST NOT rely solely on the reject/replay round-trip; it MUST retain the Announce that carried the entry, as received, in the same non-ledger holding area used for forward gaps (SYNC-14-001), keyed by the entry's prev_log_hash, pending arrival of the case seed. Unlike a forward gap (SYNC-14-001) this hold applies without a tail_index comparison — no chain tail exists in the pre-genesis window — so every entry for the missing case is held, including the genesis entry (log_index 0) whose prev_log_hash equals the per-case genesis hash.

The reject and replay recovery (SYNC-15-001) does not converge when deliveries arrive out of order, for the same reason that forward gaps are held (SYNC-14). The replay sends entries one by one over the same unordered transport, so each early entry is rejected again. This adds load, and each round can lose an entry. Holding the entry lets it drain in order once the case is seeded.
Depends on: SYNC-14-001 — Reuses the same non-ledger holding area and prev_log_hash keying as the forward-gap hold
Depends on: SYNC-14-005 — A pre-genesis entry MUST NOT be persisted as a CaseLedgerEntry while held
Refines: SYNC-15-001 — The Reject is retained as the loss backstop; holding is the primary convergence path
SYNC-15-005 MUST After a participant seeds the VulnerabilityCase (via Create or Announce(VulnerabilityCase)), which anchors the deterministic per-case genesis hash (CLP-08), it MUST drain the holding area for that case: it MUST locate the held Announce (if any) whose entry's prev_log_hash equals the per-case genesis hash, replay it (SYNC-14-009) so its entry is applied-then-persisted via the same path as in-order delivery (SYNC-14-004), and cascade in hash-chain order (SYNC-14-003) until no held Announce extends the current tail.

A participant can compute the genesis hash from the case object alone: it comes from the case id, the creation time and the Case Owner's actor id (CLP-08-002). Seeding the case therefore anchors the chain, and the participant need not wait for a second delivery of the genesis entry. Draining on seed turns the pre-genesis hold (SYNC-15-004) into convergence.
Depends on: SYNC-14-003 — Reuses the cascading drain defined for forward gaps
Depends on: SYNC-14-004 — The drained entry MUST apply effects before persisting, on the same path as in-order delivery
Depends on: SYNC-15-004 — Drains exactly the entries that SYNC-15-004 held during the pre-genesis window
SYNC-15-015 MUST When the CASE_MANAGER answers a Reject(CaseLedgerEntry) with last_accepted_hash="" with a replay, it MUST first send Announce(VulnerabilityCase) to the rejecting peer.

A peer that rejects from genesis does not hold the case, so it cannot compute the per-case genesis hash (CLP-08-005). Without the case, the peer fails the chain check on each replayed entry and rejects it again. The case lets the peer anchor its chain and drain the entries it holds (SYNC-15-005), so the replay that follows can apply.
SYNC-15-016 SHOULD NOT A participant that has sent the CASE_MANAGER a Reject(CaseLedgerEntry) with last_accepted_hash="" for a case it still does not hold SHOULD NOT send another for that case until a bounded interval has passed since the last one.

One genesis Reject asks for the case and the whole ledger. The participant holds every early entry and drains them when the case arrives (SYNC-15-004, SYNC-15-005), so a Reject for each further early entry asks for nothing the first did not. On a loaded host the CASE_MANAGER answered each of them with a full replay, and the duplicates delayed unrelated messages by about 20 seconds (#5552).
Refines: SYNC-15-001
SYNC-15-017 MUST A participant that withholds genesis Rejects under SYNC-15-016 MUST send one for the first entry of the still-missing case it receives after the interval has passed, as a new activity rather than a redelivery of an earlier Reject.

The genesis Reject is the only recovery for a participant whose replay was lost, so withholding it may delay recovery but never end it. A redelivery of an earlier Reject carries that Reject's id, and the CASE_MANAGER drops a known id as a duplicate (ID-02-001), so a request after the interval must be its own activity.
Refines: SYNC-15-016
SYNC-15-018 MUST Once a bounded interval has passed since an actor's rate limit last let through a replay, or a replay request, about the same case and position, that limit MUST let the next one through.

Every limit in this group trades recovery time for quiet. A limit that never lets a later request or replay through would turn a lost replay into a replica that never converges.
Refines: SYNC-15-003
SYNC-15-019 MAY The CASE_MANAGER MAY decline to answer a Reject(CaseLedgerEntry) with last_accepted_hash="" whose rejected entry was included in a replay from genesis it sent the same peer within a bounded interval; declining sends neither the case nor any entry.

A Reject the CASE_MANAGER handles after it already replayed that entry from genesis to the same peer asks for a replay that is already on its way. The CASE_MANAGER cannot rely on every participant to limit its own requests (SYNC-15-016), because other implementations share the wire, so it may limit them itself.
Refines: SYNC-15-003

Divergence Recovery

Under the copy model (ADR-0124) a replica applies each entry through one apply function that refuses an entry whose starting state does not match the replica's case file. When the refused entry already extends the replica's stored chain, no replay can fill the gap: the replica's case file has diverged from the CASE_MANAGER's, and the reject and replay path resends the same refused entry forever. A divergence is a defect, not a steady state, so the replica reports it and the CASE_MANAGER repairs it with a fresh case copy that names the ledger position it reflects. The fault is replication signalling, like the gap Reject, and is never ledgered. Rationale: ADR-0142.

ID Priority Requirement Related
SYNC-16-001 MUST When a participant refuses a CaseLedgerEntry whose prev_log_hash matches its chain tail because the entry's starting state does not match its case file, and its case file is not already past the entry, it MUST send Create(ProcessingFault) with failure class LedgerEntryRefused/CaseDiverged to the case's CASE_MANAGER, naming the refused Announce(CaseLedgerEntry) by reference. It MUST keep holding the refused entry as SYNC-14-001 holds a forward gap.

The entry was received but could not be processed, which MSM-05-001 assigns to a processing fault. Without it the CASE_MANAGER sees only gap Rejects, and its replay resends the entry the replica refuses. An entry whose content is invalid in itself, such as the impossible participant status of RSH-09-006, is not a divergence: a fresh copy cannot repair it, so it takes that requirement's fault instead of this one.
Refines: ASK-07-001
SYNC-16-002 MUST NOT A processing fault that reports a failure to apply a CaseLedgerEntry MUST NOT be committed to the case ledger, by the participant or by the CASE_MANAGER. The CASE_MANAGER's response to it is per-peer replication state (SYNC-04), except for the hash-mismatch fault, which changes nothing (SYNC-03-008).

It reports on replication, as Reject(CaseLedgerEntry) does. Ledgering it loops: the diverged replica cannot apply the entry that records its own fault, and a failure to apply that entry would be another fault to ledger.
Refines: CLP-04-007
Constrains: RSH-09-006
SYNC-16-003 MUST When the CASE_MANAGER receives the SYNC-16-001 fault from an active participant of the case (CM-10-004), it MUST send that participant Announce(VulnerabilityCase) of its current case. From any other sender it MUST refuse the fault, send nothing and record no replication state for the sender.

The CASE_MANAGER's case is authoritative (PCR-03-001), and a copy carrying its ledger position (SYNC-16-005) lets the replica replace its case file and backfill (SYNC-16-006, SYNC-16-007). The sender gate is the active-participant standing (CM-10-004, CM-23-020), not the gap Reject's (SYNC-03-005): that one admits a closed participant, but a copy of the current case would cross the closure cutoff such a participant is held to (CM-23-004), so its diverged replica is not repaired this way.
Depends on: SYNC-16-005
SYNC-16-004 MUST The CASE_MANAGER MUST bound the rate at which it re-seeds a peer under SYNC-16-003. It MUST record, per peer, the time and ledger tail of the last re-seed, and a fault received within the cooldown MUST NOT trigger another re-seed; a fault after it MUST.

A diverged replica faults once per replayed entry it refuses. A rate limit stops a re-seed storm without stranding a peer whose re-seed was lost, which a permanent suppression would (SYNC-15-003 makes the same choice for replay).
Refines: SYNC-16-003
SYNC-16-005 MUST Each VulnerabilityCase MUST store its ledger position (ledgerPosition), the position its case file is a projection of: the log_index and entry_hash of the last entry applied to it, or, before any, index -1 and the per-case genesis hash. Committing an entry, or storing one as a replica, MUST move the position to that entry atomically with storing the entry, unless the position is already past it or names it, which MUST leave the position unchanged. A ledger entry whose payloadSnapshot carries the case MUST carry it at the position before that entry. The position MUST travel with the case on its wire form, so a copy names the position it reflects, and a received case that carries none MUST be refused.

A case copy is the case as applying the ledger up to some entry leaves it (ADR-0124); the position names that entry, so a receiver can tell a current copy from a stale one. Stored on the case, it is right on a replica too, whose case can be ahead of its own chain after it replaces its case with a copy. Moving it with the entry it names keeps the two from disagreeing, and a snapshot taken before the entry keeps any entry's hash from depending on itself. Never moving it back keeps a case a copy put ahead of its chain at the copy's position while the chain catches up (SYNC-16-007); a different entry at the position's index is a conflict, not one it already reflects.
Refines: PCR-02-001
SYNC-16-006 MUST A participant that receives Announce(VulnerabilityCase) from the case's CASE_MANAGER for a case it already holds, a repeat of an earlier Announce included, MUST compare the copy's ledger position (SYNC-16-005) with its own: the later of its chain's tail and its case's stored position. If its own is already past the copy's, it MUST leave its case file unchanged, as for a repeat delivery. If it holds an entry at the position's index, or its case names one there, whose hash differs, it MUST leave its case file unchanged and refuse the copy. Otherwise it MUST replace its case file with the copy, taking the copy's position.

The transport is unordered and the outbox retries, so an old copy can arrive late; replacing with it would drop the effects of the entries the replica already holds past the copy. A different entry at the same index means the chains themselves disagree, which no copy repairs. The case's stored position counts as well as the chain, because after a replacement the case can be ahead of the chain, and a copy older than the replacing one must not move it back.
Refines: PCR-02-004
SYNC-16-007 MUST After replacing its case file under SYNC-16-006 with a copy whose position is ahead of its chain, a participant MUST send Reject(CaseLedgerEntry) carrying its own tail. A participant whose case is at a position ahead of its chain, after such a replacement or a first seed from a copy (PCR-02-003), MUST store each entry it receives at or before that position, whether replayed or drained from its holding area (SYNC-14-003), as an idempotent no-op, not refuse it, and MUST NOT move the case's position back for it (SYNC-16-005). It MUST refuse, not store, an entry at the position's index whose hash differs from the one the position names, and log the conflict at ERROR. When its chain stores no entry, the Reject MUST name no rejected entry, MUST state the empty chain's ledgerPosition (index -1, SYNC-18-001) and MUST name the case as its target, asking for a replay from genesis; a receiver MUST refuse a Reject that names no entry and states no position, or a position other than the empty chain's, and the CASE_MANAGER MUST answer the empty-chain form as it answers any replay from genesis (SYNC-15-015).

Those entries are already reflected in the copy. The refused entry the replica held under SYNC-16-001 drains first, before any replay arrives, so a rule that covered only replayed entries would refuse it again. Storing them advances the chain to the copy's position, after which the entries that follow apply normally, so the replica ends where seed-plus-replay leaves it (ADR-0114). A first seed is the same case: the copy already reflects the entries the joining replica's backfill brings. The entries before the position carry no hash the case names, so a chain that diverged among them shows only at the position's index; refusing the entry there keeps the chain from naming one entry and the case another. A replica with no entry has none for the Reject to name; waiting for the next fan-out would stall it on a quiet case, so it names the case instead and asks for everything.
Depends on: SYNC-16-006
Refines: SYNC-03-002

Participant Position Statements

A participant's replica can fall behind with nothing to show it: when the last Announce(CaseLedgerEntry) before a quiet period is lost, no later entry arrives with a gap, so no Reject(CaseLedgerEntry) is sent and the reject-and-replay loop never starts. The CASE_MANAGER cannot see the lag either, because nothing tells it how far a participant has got. The participant therefore states its position, when it chooses, in a Read(CaseLedgerEntry), and the CASE_MANAGER answers a position behind its tail by replaying. The position stated on every case activity (SYNC-18) composes the same answer. This is how the LedgerReconciliation phase detects a gap and retries until convergence (SYNC-00-003, SYNC-00-007, SYNC-00-008). Rationale: ADR-0155.

ID Priority Requirement Related
SYNC-17-001 MAY A participant MAY send the case's CASE_MANAGER Read(CaseLedgerEntry) whose object is the last canonical entry it has applied for that case, by reference. A participant that has applied no entry for the case has no entry to name and sends no Read(CaseLedgerEntry).

ActivityStreams defines Read as the actor having read the object, so the activity states a fact and asks for nothing. A Reject(CaseLedgerEntry) would claim a refusal that did not happen.
SYNC-17-002 MUST The CASE_MANAGER MUST act on a participant's stated ledger position in Read(CaseLedgerEntry) (SYNC-17-001) only when the sender is an active participant in the case (CM-10-004). From any other sender it MUST record no replication state for the sender, send nothing in answer, and report REFUSED.

An inert or closed participant's replica stops on purpose, so its stated position always lags, and answering it would send withheld case content (SYNC-03-005). CM-10-006's backfill catches up a participant that becomes active again. The Invite resend is no answer to the position and runs apart from this check (SYNC-20-001, ADR-0163): an unconsented participant on the roster is resent the Invites it was already sent, which carry no withheld content, and its statement is otherwise refused.
Refines: CM-10-004
Refines: HP-01-006
SYNC-17-003 MUST When the CASE_MANAGER acts on a stated position (SYNC-17-002) that names an entry in its ledger, it MUST record that entry's hash as the sender's acknowledged position in its per-peer replication state (SYNC-04-001).

The CASE_MANAGER has no other record of how far a quiet participant has got (SYNC-04-001). The stated entry is a fact the participant reports about itself, so recording it gives the replay a correct start and the cooldown a correct position.
Refines: SYNC-04-001
SYNC-17-004 MUST When the position the CASE_MANAGER acts on (SYNC-17-002) is behind its ledger tail, it MUST send the sender every canonical entry after that position, in log order, as Announce(CaseLedgerEntry), under the same replay rate bound (SYNC-15-003, SYNC-15-013) and outbox de-duplication (SYNC-15-012) as a Reject-triggered replay. When the position is the tail it MUST send nothing.

This is the retry that SYNC-00-008 requires. It runs whenever the participant reads, so a lost last entry is recovered without waiting for a new commit. A Read can predate entries already on their way to the sender, so a stated lag can be transient; the replay rate bound, not the outbox de-duplication, is what limits the replays such a race causes.
Refines: SYNC-00-007
Refines: SYNC-00-008
Refines: SYNC-03-002
SYNC-17-005 MUST When a stated position names an entry the CASE_MANAGER's ledger does not hold, or an index at which its entry has a different hash, the CASE_MANAGER MUST log it at WARNING with the sender and the position, record no replication state and send nothing.

Such a position is not a lag, and replaying from a guessed point would send a suffix the replica cannot anchor. A replica whose case file has diverged reports it by SYNC-16-001.
SYNC-17-006 MUST NOT The CASE_MANAGER MUST NOT commit a ledger entry for a Read(CaseLedgerEntry) or for its answer to one.

Stating a position changes nothing in the case file, so it is an observation and not a case event (ADR-0152). Ledgering it would add an entry every time a participant checks, which itself would leave every other participant behind.
SYNC-17-008 SHOULD Once an Announce(VulnerabilityCase) has seeded a participant's replica of a case it did not hold (PCR-02-003), the participant SHOULD send the case's CASE_MANAGER one Read(CaseLedgerEntry) (SYNC-17-001) naming the entry at the ledger position the seeded case is at (SYNC-16-005).

A joiner that dropped an Invite before its case copy arrived (CBT-03-003) may have nothing else to send, so the CASE_MANAGER never learns that its replica is ready. The statement tells it, and the CASE_MANAGER can then resend each Invite the participant has not answered (ADR-0163). The seeded case reflects every entry up to the copy's position, and the replica stores none of them yet (SYNC-16-007), so that position is the replica's tail. A copy that updates a held replica tells the CASE_MANAGER nothing new, and SYNC-16-007 already asks for what it lacks.

Sender Ledger Position on Case Activities

Every case activity states the ledger position its sender held when it built the activity, so the case ledger records what each actor knew when it acted. The position is a record, not a gate: an actor that lags the CASE_MANAGER's latest entry may still act (SYNC-10-005), and a stale answer to a replaced object is already refused by the object it names (CM-18-003). A lagging position is answered as a Read(CaseLedgerEntry) is (SYNC-17), and a position the CASE_MANAGER's ledger does not hold is a split chain, which it repairs. Rationale: ADR-0156.

ID Priority Requirement Related
SYNC-18-001 MUST An activity about a case whose ledger its sender holds, sent by any actor, MUST carry the sender's ledger tail for that case as the activity's ledgerPosition: an object whose logIndex and entryHash name the last entry the sender had committed, or had stored as a replica, when it built the activity; for an empty ledger, index -1 and the per-case genesis hash. A Read(CaseLedgerEntry) carries it as the position of the entry it names, which is that tail (SYNC-17-001). An activity sent before its sender holds the case's ledger, an activity whose object already states a ledger position (Announce(CaseLedgerEntry), Announce(VulnerabilityCase), Create(VulnerabilityCase), Update(VulnerabilityCase)), and a Create(ProcessingFault) MUST NOT carry one.

One rule for every sender and message type gives every ledgered act its provenance, and a new message type needs no decision about whether it counts. A stub-Invite reply or a report Offer comes from an actor with no ledger to state. An entry and a case copy already state their own positions, and Create(VulnerabilityCase) and Update(VulnerabilityCase) are copies too. Join, Ignore, Leave and Offer of a case name the case and act on it, so they carry the sender's position. A Create(ProcessingFault) is about the processing of one activity, not about a case. Any receiver sends one, including for an activity it could not parse and so cannot place in any case (ASK-07-001), and it names no case (ASK-07-004). This applies to the replica's faults too. The property is the ledgerPosition term that a case copy already carries (SYNC-16-005), so one fact keeps one wire form.
Refines: SYNC-16-005
SYNC-18-002 MUST ledgerPosition MUST be the sender's ledger tail for the case at the moment the activity is built, and MUST be produced only by vultron.wire.as2.factories. A resent activity MUST carry the position it was first sent with.

The position describes the moment of the act, so a later resend that restamped it would misstate what the sender knew. The outbox already relays the sealed body (OX-07-001), which keeps the first position.
Refines: SYNC-18-001
SYNC-18-003 MUST A receiver MUST parse ledgerPosition as the core LedgerPosition at the parse edge, and MUST refuse an activity that SYNC-18-001 requires to carry one when its ledgerPosition is missing or does not parse, naming the activity and writing nothing. A Reject(CaseLedgerEntry) that names an entry and carries no position MUST NOT be refused for it: it comes from a peer that does not hold the case yet, which the CASE_MANAGER seeds and replays from genesis (SYNC-15-015).

Core code then receives a non-optional position (ADR-0032), and every ledgered act carries one. A receiver cannot tell from a Reject(CaseLedgerEntry) alone whether its sender holds the case, and the peer that does not is the one the replay from genesis exists to repair.
Refines: SYNC-18-001
SYNC-18-004 MUST NOT The CASE_MANAGER MUST NOT refuse, hold or defer an activity because its ledgerPosition is behind the CASE_MANAGER's own ledger tail, and MUST log such a lag at DEBUG with the activity and actor. A reply to the full-case Invite is the one exception, judged against its Invite's floor (CM-11-012).

Under asynchronous fan-out an active participant is routinely a few entries behind, and SYNC-10-005 lets it act while it lags. A stale answer to a replaced embargo is already refused, because its consent row accepts no trigger (CM-18-003), so refusing on the position alone would refuse ordinary acts and catch nothing new.
Refines: SYNC-10-005
SYNC-18-005 MUST When an activity's ledgerPosition names an index the CASE_MANAGER's ledger holds with a different entry hash, or an index past its own tail, the CASE_MANAGER MUST judge the activity as it would without the position, MUST log the split at ERROR, and, when the sender is an active participant of the case (CM-10-004), MUST send it Announce(VulnerabilityCase) of its current case under the rate bound of SYNC-16-004. It MUST NOT re-seed any other sender.

The CASE_MANAGER is the single writer, so no participant can hold an entry it did not commit; such a position means the sender's chain has split. The fresh copy repairs it with the path SYNC-16-003 already uses for a reported divergence. A Read(CaseLedgerEntry) naming such a position is answered with nothing instead (SYNC-17-005); that difference, and whether the act itself should be refused, are open (notes/sync-ledger-replication.md).
Refines: SYNC-16-003
SYNC-18-006 MUST The ledgerPosition of an admitted activity MUST be recorded in its ledger entry as part of the received body (CLP-07-011), and MUST NOT be removed from it.

The entry is the received envelope with a postmark (ADR-0107), so the position is ledgered with the act it describes and replicas receive it unchanged.
Refines: CLP-07-011
SYNC-18-007 MUST When an activity's ledgerPosition names an entry in the CASE_MANAGER's ledger and the sender is an active participant of the case (CM-10-004), the CASE_MANAGER MUST answer the position as it answers one stated in Read(CaseLedgerEntry): it MUST record the entry's hash as the sender's acknowledged position (SYNC-17-003) and, when the position is behind its tail, send the entries after it under the replay rate bound and de-duplication of SYNC-17-004. From any other sender it MUST record no replication state and send nothing in answer to the position. Neither the record nor the replay changes the activity's own outcome.

A stamp states the same fact as a Read(CaseLedgerEntry), so one answer serves both (ADR-0155): a participant that sends anything also tells the CASE_MANAGER how far it has got, and one that lost the last entry is repaired by its next act. A removed, unconsented or closed participant lags on purpose, so answering it would send withheld case content (SYNC-17-002).
Extends: SYNC-17-003
Extends: SYNC-17-004

Arrival-Order Independence and Eventual Consistency

The transport promises no delivery order (ADR-0037), and other implementations may share the wire, so every message a case actor receives can arrive before, between or after the messages it relates to. The protocol's guarantee is eventual consistency: the case ledger's log_index is the causal order (CLP-14-001), and hash-chain apply plus gap repair bring every replica to the CASE_MANAGER's case file. No decision may lean on an arrival order the ledger does not define. Rationale: ADR-0161.

ID Priority Requirement Related
SYNC-19-001 MUST NOT A coordination decision MUST NOT depend on an arrival order that the case ledger does not define. The CASE_MANAGER's observation order is the case's causal order (CLP-14-001): when it takes concurrent assertions from different senders in the order it observes them, its ledger records that order and every replica learns the same outcome. Every other decision MUST be the same whatever order its messages arrive in: what a participant applies, stores or answers, given a message the CASE_MANAGER sent outside the ledger and the ledger entries it relates to; and what the CASE_MANAGER records, given two assertions one sender sent before the first was committed.

Arrival order differs between recipients, between implementations and between runs. A decision that reads it gives different coordination outcomes for the same messages, and the ledger cannot explain the difference. The CASE_MANAGER cannot observe the order one sender sent two messages in, so that order is lost unless the decision does not need it.
Depends on: CLP-14-001
SYNC-19-002 MUST Eventual consistency MUST be the ordering guarantee the protocol makes: every active participant's replica that receives the CASE_MANAGER's committed ledger entries, in any order, MUST reach the CASE_MANAGER's case file through hash-chain apply (SYNC-03-001, SYNC-14) and gap repair (SYNC-00-007, SYNC-00-008, SYNC-16, SYNC-17). A message the CASE_MANAGER sends outside Announce(CaseLedgerEntry) MUST NOT leave a replica in a final state that the ledger entries it relates to do not determine.

Replicas see the case at different times, so a stronger guarantee would need a delivery order the transport does not give. A direct message that wrote state the ledger later cannot correct would let two replicas holding the same ledger disagree for good.
Refines: SYNC-08-002
SYNC-19-003 MUST NOT Per-recipient delivery order MUST NOT be a protocol guarantee. A receiver MUST NOT rely on a sender delivering its messages in the order it sent them, and a sender MUST NOT rely on its own ordered delivery for a receiver's correctness. An implementation MAY deliver in order per recipient as an optimization (OX-01-005).

Another implementation may share the wire with no ordered outbox, and even an ordered outbox loses that order when an Announce is lost and replayed later, so a rule that holds only under ordered delivery fails in exactly the cases recovery exists for.
Constrains: OX-01-005

Resending Unanswered Invites

An Invite can be lost before its addressee can answer it. A joiner's relayed Invite(EmbargoEvent) or full-case Invite that arrives before its case copy waits in the pre-bootstrap queue, and is dropped when the queue's window ends (CBT-03-003); any Invite can be lost in delivery. A participant that states its ledger position tells the CASE_MANAGER its replica holds the case, which is the moment a resent Invite can be answered. So the CASE_MANAGER resends each Invite the participant still owes an answer to, as first sent and rate-bounded per Invite and participant. Rationale: ADR-0163.

ID Priority Requirement Related
SYNC-20-001 MUST When a participant on the case's roster that is not removed (CM-31-001) and whose closure is not recorded states a ledger position that names an entry in the CASE_MANAGER's ledger, in Read(CaseLedgerEntry) (SYNC-17-001) or as an activity's ledgerPosition (SYNC-18-001), the CASE_MANAGER MUST resend each Invite that participant owes an answer to (SYNC-20-002), whether or not the participant is active (CM-10-004).

A position statement is the first sign that the replica holds the case, so a resent Invite can now be answered (ADR-0163). The resend is not an answer to the position, so it runs apart from SYNC-17-002 and SYNC-18-007: a joiner that lost its Invite to the embargo in force has not consented, so it is inert, and is the one to ask again. A resent Invite was already sent and is not case content (CM-10-007), so nothing withheld leaks.
Refines: SYNC-17-002
Refines: SYNC-18-007
Depends on: CM-10-007
SYNC-20-002 MUST An Invite the CASE_MANAGER sent a participant MUST be treated as owed an answer while that answer can still count, and at no other time: an Invite(EmbargoEvent) whose embargo is still a proposal or is the embargo in force, whose consent row for the participant is INVITED (CM-18-001), and whose RSVP deadline has not passed (CM-28-001); and the full-case Invite (CM-11-010) until the CASE_MANAGER's ledger records the participant's reply to it. Of several Invites for one embargo, only the last one sent is owed, and the Invite that the activity stating the position answers is not owed.

Only an answer that can still change something is worth asking for again; an answered Invite, a decided or withdrawn proposal and a passed deadline cannot. An Invite for the embargo in force is still owed: a reinstated participant (CM-31-040), or a joiner whose proposal was activated first, is asked to sign it. The stating activity may be the answer itself, committed after its position is read, so its Invite is excluded by name.
Refines: SYNC-20-001
SYNC-20-003 MUST A resent Invite MUST be the activity as first sent: the same id, ledgerPosition and RSVP deadline (SYNC-18-002), delivered from the body sealed when it was first sent (OX-07-001), never a new Invite built for the resend. So an Invite whose sealed body the CASE_MANAGER does not hold is not resent.

A receiver that already handled the Invite treats a redelivery as a no-op (IE-10-001), and one that dropped it handles it as new. Resending never extends a deadline, so silence before the deadline is still a pocket veto, and the full-case Invite's ledger floor (CM-11-012) is unchanged.
Refines: SYNC-18-002
Depends on: OX-07-001
SYNC-20-004 MUST NOT The CASE_MANAGER MUST NOT resend an Invite to a participant while an earlier send of that Invite is still pending in its outbox; such a skipped resend does not count toward its cooldown (SYNC-20-005).

An Invite still waiting in the outbox has not been lost, so a second copy would only queue behind it. Not counting the skip keeps a later, real resend from waiting out a cooldown no delivery started. This is the replay's rule for a pending Announce (SYNC-15-012).
Refines: OX-06-002
SYNC-20-005 MUST Each consecutive resend of one Invite to one participant MUST double the cooldown before the next resend of that Invite to that participant, from a base value up to a fixed ceiling; a position stated inside the cooldown resends nothing.

A participant that leaves an Invite unanswered on purpose states its position again and again. A doubling cooldown soon makes the resends rare, and the ceiling still resends at a fixed interval until the deadline passes, as the replay bound does (SYNC-15-013).
Refines: SYNC-15-013
SYNC-20-007 MUST NOT A resent Invite MUST NOT be committed as a ledger entry, nor change the outcome of the activity that stated the position.

The Invite's emission was ledgered when it was first sent, so a resend changes nothing in the case file and is not a case event (ADR-0152). The activity that stated the position is judged on its own merits, as SYNC-18-007 requires of the rest of the answer to its position.
Refines: SYNC-17-006
SYNC-20-008 MUST NOT The CASE_MANAGER MUST NOT resend an Invite to a sender that is not on the case's roster, that was removed (CM-31-001), or whose closure the case records.

A removed participant was taken out by the Case Owner's decision, and inviting it again would undo that decision (CM-31-013). A closed participant may act on nothing further (CM-23-018), and an actor off the roster was never sent an Invite to answer.
Refines: SYNC-20-001
Depends on: CM-31-013

Participant Case Replica

Requirements for how each Vultron Actor maintains and manages its local copy (replica) of a VulnerabilityCase. Covers replica identity, bootstrap, update authority, case-context message routing, reporter case discovery, and unknown-context handling.

Replica Identity (No Stub CASE_MANAGER)

ID Priority Requirement Related
PCR-01-001 MUST Each Vultron Actor MUST maintain its local case replica as an internal concern — there is exactly one externally-addressable Actor identity per participant, regardless of how many cases that Actor is involved in.

Other actors address an Actor, and decide whether to trust it, by its one id. A separate identity for each case would need its own discovery and its own trust anchor for every case, and a message sent to the Actor's main id would not reach the case. Keeping each replica inside the Actor gives every case the same inbox and the same identity.
Refines: CM-01-001
Refines: AKM-01-002
PCR-01-002 MUST There is exactly one CASE_MANAGER per case, acting on the Case Owner's behalf.

The CASE_MANAGER is the single writer of the case ledger. Two CASE_MANAGERs could each commit a different entry at the same index, and the replicas would then disagree with no rule to pick one. A participant also needs exactly one sender whose case updates it accepts (PCR-03-001).
Refines: CM-02-001
PCR-01-003 MUST Actor co-location is an implementation choice the protocol MUST remain indifferent to. Whether every Actor runs on one host or on many, Actors MUST exchange information only through protocol messages. An Actor MUST NOT read, write, or otherwise reach into another Actor's store, and MUST NOT rely on out-of-band communication with a co-located Actor. In particular, an Actor that has asked a CASE_MANAGER to create a case MUST treat the case as absent from its own knowledge until the CASE_MANAGER's Create(VulnerabilityCase) replica has been delivered to it, and MUST NOT assume co-location makes the case visible sooner.

Co-location is a deployment choice. Code that reads another Actor's store, or waits for a side channel, works only while two Actors share a host. Per-actor stores (ADR-0073) prevent direct reads and writes, but not code that expects the case to be visible early. The report receiver does not create the case (ADR-0041), so it holds the case only after the CASE_MANAGER's Create(VulnerabilityCase) arrives. A tree that acts on the case sooner has nothing to act on.
Refines: CM-01-001
Refines: AKM-01-002
Implements: DL-07-001

Replica Bootstrap

ID Priority Requirement Related
PCR-02-001 MUST Announce(VulnerabilityCase) is the unified mechanism for delivering a full case snapshot to a participant, at every lifecycle stage.

A participant that gets every full case copy in one activity type applies each copy through one path, with one sender check (PCR-03-001) and one ledger-position check (SYNC-16-006). Two delivery types would need two sets of checks, and a gap in either set would let a copy in that the other set refuses.
Implements: AKM-03-001
Implements: SYNC-02-001
PCR-02-002 MUST When a participant becomes active (CM-10-004) — at case creation, or on accepting the stub Invite (CM-11-008) — the CASE_MANAGER MUST send Announce(VulnerabilityCase) to that participant to deliver its initial case copy. It MUST NOT send one to an inert participant.

A participant cannot act on a case it does not hold, and only the CASE_MANAGER holds the canonical copy. Sending the copy when the participant becomes active gives it the case at the moment it is entitled to it. An inert participant is not entitled to case content (CM-10-004), so it gets no copy.
Implements: CM-06-001
PCR-02-003 MUST When a participant actor receives Announce(VulnerabilityCase) and has no existing local copy for that case ID, it MUST create a local replica from the received case object.

The first case copy a participant receives is its only source for the case. If the participant did not store it, every later ledger entry for the case would name a case the participant does not hold.
PCR-02-004 MUST When a participant actor receives Announce(VulnerabilityCase) and already has a local replica for that case ID, it MUST apply the received case state as an authoritative update, subject to PCR-03-001 and to the ledger-tail comparison of SYNC-16-006.

An update is safe only when the copy names the ledger position it reflects, because an old copy can arrive late over an unordered transport (ADR-0142).
PCR-02-005 MUST NOT Create(VulnerabilityCase) retains its semantic meaning ("a case was created") but MUST NOT be used as the mechanism for delivering a full case snapshot to participants.

Create(VulnerabilityCase) says that a case exists. If it also delivered full copies, a receiver would have two kinds of copy to apply, and each would need the sender check (PCR-03-001) and the ledger-position check (SYNC-16-006). One delivery type for copies (PCR-02-001) keeps those checks in one place.

Replica Update Authority

ID Priority Requirement Related
PCR-03-001 MUST A participant actor's local case replica MUST only accept case-state updates that originate from the CASE_MANAGER for that case.

The CASE_MANAGER is the single writer of the case. A replica that accepted a case-state update from any other sender would let that sender change the participant's view of the case without the CASE_MANAGER's checks and without a ledger entry, and the replicas would then disagree.
Implements: CM-06-002
PCR-03-002 MUST NOT The Case Owner actor MUST NOT write directly to its own local case copy.

When the Case Owner is not the CASE_MANAGER, its copy of the case is a replica like every other, and it changes only after the CASE_MANAGER has checked the change and recorded it in the ledger. A direct write skips both steps, so the other replicas never learn of the change and the owner's copy then disagrees with the canonical case.
Refines: CM-02-010
PCR-03-003 MUST A participant actor MUST authenticate Announce(VulnerabilityCase) activities before accepting them as authoritative updates to its local replica.

The sender check of PCR-03-001 compares the activity's actor id, and a sender writes that id itself. Without authentication, any sender can write the CASE_MANAGER's id there and have its copy applied. Actor signatures are not yet available, so this rule applies to production only.
PCR-03-004 MUST NOT A participant actor MUST NOT create a local case replica from an Announce(VulnerabilityCase) message unless prior trust for that case has been established: either (a) the actor has already processed a Create(VulnerabilityCase) from the report receiver that introduced the CASE_MANAGER identity, or (b) the actor has completed an InviteActorToCase exchange (the Invite and its Accept) that records a pending-expectation for that case. Accepting any Announce(VulnerabilityCase) from an unauthenticated or unexpected sender as a bootstrap signal is not permitted. For path (b), the pending-expectation record MUST be created only when the Invite's object is the receiving actor; the record MUST bind to the Invite's actor id (the named CASE_MANAGER), not to the transport-level delivering sender; and a later Invite for the same case naming a different CASE_MANAGER MUST replace the anchor while the receiver has not accepted the Invite that set it, and MUST be refused, leaving the anchor unchanged, once the receiver has accepted that Invite. Whether the named CASE_MANAGER is genuine cannot be verified without actor identity and signatures (#2841); this requirement records the structural rules that apply before that verification is available.

An Announce(VulnerabilityCase) roster is supplied by the sender; a fabricated case naming the sender as its own CASE_MANAGER passes a roster-only check. Permitting a first-contact Announce when no locally derived record exists lets an attacker pre-seed a fake replica, blocking the real CASE_MANAGER on the seeded path (PCR-03-001 fails closed). No legitimate protocol sequence delivers an unsolicited first-contact Announce without a preceding Create(VulnerabilityCase) or InviteActorToCase. The path (b) sub-rules prevent a misaddressed Invite from anchoring trust on behalf of a different actor and prevent a second Invite from replacing the anchor the receiver has accepted. Until the receiver accepts, a later Invite replaces the anchor, so a forged stub Invite cannot lock out the real CASE_MANAGER's Invite until the bootstrap; a forged Invite the receiver accepts first still wins, a residual risk accepted until signatures (#2841).
Refines: PCR-03-001
Refines: PCR-02-003
PCR-03-005 MUST The case owner actor MUST follow the same replica rules as all other participants.

Exempting the case owner from replica rules would create an inconsistent update path that bypasses the CASE_MANAGER consistency checks.
Refines: PCR-03-002
PCR-03-006 MUST The case owner actor MUST route all case-state changes through the CASE_MANAGER.

Routing through the CASE_MANAGER ensures all case-state mutations go through the single-writer authority and are recorded in the case ledger.
Refines: PCR-03-002

Case Context Routing

ID Priority Requirement Related
PCR-04-001 MUST The context field of an ActivityStreams activity is the canonical mechanism by which a participant actor identifies the local case replica a case-scoped message belongs to.

An Actor holds replicas of many cases behind one inbox (PCR-01-001), so each case-scoped activity must say which case it is about. One standard field for that lets every receiver find the replica the same way.
Implements: CM-01-002
PCR-04-002 MUST All case-scoped protocol activities sent by the CASE_MANAGER to participants MUST carry context set to the case ID.

A participant finds the replica an activity belongs to from its context (PCR-04-001). An activity from the CASE_MANAGER with no case context also skips the hold for unknown cases (PCR-06-002), so the participant can process it before it holds the case.
PCR-04-003 MUST NOT The participant actor MUST be the only externally-addressable entity for receiving case-scoped messages on behalf of all cases it participates in; there MUST NOT be a separate per-case inbox.

Other actors reach a participant at its one Actor identity (PCR-01-001). A separate inbox for each case would be a second address that senders must discover and trust for every case, and a message sent to the wrong address would not reach the replica.

Reporter Case Discovery

ID Priority Requirement Related
PCR-05-001 MUST Each actor MUST keep, for each report it has submitted, a link from the report's id to the id of the case created for it.

A reporter learns about the case for its report only from the activities it receives later. The link from the report to the case lets it find the case for the report it submitted, and it lets the reporter check that a case it receives is the case for that report (CBT-01-005).
PCR-05-002 MUST When a participant actor receives Announce(VulnerabilityCase) and the case references a report whose report-to-case link is not yet bound to a case, the actor MUST bind that link to the local case replica.

The report-to-case link is how a reporter finds the case for its report (PCR-05-001). If a received case copy did not update the link, the reporter would hold the case but could not find it from the report. A bound link also records the CASE_MANAGER trusted for its case (CBT-01-006), so a link already bound to a case is left on it. Moving it to a second case holding the same report would take that trust from the first case and give the second case a CASE_MANAGER it never had, which CBT-02-003 forbids.

Unknown Case Context Handling

ID Priority Requirement Related
PCR-06-001 MUST NOT When a participant actor receives an activity whose context field does not match any known local case replica, the actor MUST NOT silently discard the activity.

Activities can arrive before the case they name, because the transport does not keep order (ADR-0161). The receiver has already taken delivery, so the sender has no reason to send the activity again, and a discarded activity is lost. Keeping it, or reporting the drop, keeps the participant from losing a case change without a record.
PCR-06-002 SHOULD When an activity with an unknown case context arrives, the actor SHOULD queue the activity briefly.

An activity for an unknown case usually arrives before the case copy that would let the participant apply it. Holding it for a short time gives that copy time to arrive. The participant does not ask for the copy: the CASE_MANAGER sends it when the participant joins (CM-11-008), and resends an Invite the participant dropped once the participant states its ledger position (SYNC-17-008, ADR-0163).
PCR-06-003 MUST NOT Activities with unknown case context MUST NOT update any local state until a valid case replica has been established.

Until a replica exists, the participant has nothing to check an activity against: not the case, not its CASE_MANAGER, not its roster. A write made then could seed case state from an untrusted sender, which PCR-03-004 forbids for the case itself.
Depends on: PCR-06-005
PCR-06-005 MUST A stub Invite (CM-11-013) MUST be processed on arrival, not queued under PCR-06-002, even though its receiver holds no replica of the case it names. Processing it MUST write nothing but the intake archive of the Invite (CLP-10-017) and the trust anchor of PCR-03-004 path (b). Every other activity naming a case the receiver does not hold is still queued.

An invitee decides whether to join a case before the case is delivered to it (MV-10-005), and the case arrives only after it accepts. An Invite held until the case arrives would wait for the Announce that only its own Accept can bring, so the invitee could never answer it, except from a copy stored under the sender's id, which DL-06-006 forbids. The Invite writes only its intake archive and its trust anchor, so it sets no case state before the replica exists (PCR-06-003).
Refines: PCR-06-002
Depends on: PCR-03-004

Outbound Participant Message Routing

ID Priority Requirement Related
PCR-08-001 MUST After a case is created and the CASE_MANAGER is introduced, a participant actor MUST address all case-scoped outbound messages (notes, status updates, embargo proposals, RM transitions, and any other protocol messages targeting the case) exclusively to the CASE_MANAGER for that case. A participant actor MUST NOT send these messages directly to other participants. The one exception is RK, Read(Offer(VulnerabilityReport)): it answers the reporter's original Offer, outside the case, so an addressee of that Offer sends it to the reporter directly and never to the CASE_MANAGER (RMB-08-003).

A participant sends each case-scoped message to the CASE_MANAGER, so that the CASE_MANAGER can check the sender, record the change in the case ledger and send it on to the other participants (PCR-08-003). A message sent straight to a peer gets none of these steps, so the peer's replica and the canonical case disagree. RK is not a case message: it answers the reporter's Offer, which the reporter sent to that receiver before the case existed, so the answer goes back to the reporter (RMB-08-003).
PCR-08-002 MUST NOT A participant actor MUST NOT assume that other participants will receive its case-scoped messages through any channel other than the CASE_MANAGER's Announce(CaseLedgerEntry) broadcast. Sending to the whole roster or any multi-recipient list that includes non-CASE_MANAGER actors is a violation of this rule.

Only the CASE_MANAGER's Announce(CaseLedgerEntry) tells a participant what the case ledger holds. A participant that sends its message to other participants as well gives them a change that the CASE_MANAGER has not yet checked or recorded, and they may act on a change that the CASE_MANAGER later refuses.
PCR-08-003 MUST The CASE_MANAGER MUST, for every participant message it receives and accepts, commit a CaseLedgerEntry recording the outcome and broadcast Announce(CaseLedgerEntry) to the case's active participants (CM-10-004) as the standard cascade. This cascade is the only mechanism by which participants learn of accepted case-state changes.

Participants learn of case changes only from the ledger. If the CASE_MANAGER applied a change without an entry, or recorded an entry without sending it, the other replicas would never learn of the change and would disagree with the canonical case.
PCR-08-013 MUST The CaseLedgerEntry commit and Announce(CaseLedgerEntry) broadcast (PCR-08-003) MUST follow automatically from the CASE_MANAGER processing any accepted participant message, without any further request or operator action.

If the commit and the broadcast needed a separate request, a CASE_MANAGER that did not get the request would apply a change that no replica learns about. Making them part of processing the message means every processed change reaches the ledger and the participants.
Refines: PCR-08-003
PCR-08-005 MUST A participant sending a case-scoped message MUST take its recipient from its replica of the case: the participant holding the CASE_MANAGER role, as the message's sole to: recipient. The case's full participant list MUST NOT serve as the recipients of such a message.

The case's full participant list includes actors that are not entitled to every message (CM-10-004), and it reaches peers that the CASE_MANAGER has not yet checked the message for. Taking the CASE_MANAGER from the participant's own replica gives one recipient that the participant already trusts.
PCR-08-007 MUST An Invite that invites an actor to a case (InviteActorToCase) MUST be sent with actor set to the CASE_MANAGER's ID, not the Case Owner's ID. The Case Owner triggers the invite action, but the CASE_MANAGER MUST be the ActivityStreams actor performing the send. Optionally, attributedTo on the activity MAY carry the Case Owner's ID to record who initiated the invitation.

A receiver records the Invite's actor as its trust anchor (PCR-03-004), and it later accepts the case copy only from that actor. If the Case Owner were the actor, the invitee would anchor its trust on the wrong actor and answer the wrong actor. attributedTo keeps a record of who asked for the Invite.
Refines: PCR-08-001
PCR-08-008 MUST The invitee's Accept of that Invite (PCR-08-007) MUST be addressed to the CASE_MANAGER (not the Case Owner). The CASE_MANAGER is the authoritative recipient and processor of all case-management handshake messages after case creation.

Only the CASE_MANAGER records the invitee's join in the case and in the ledger. When the Case Owner is not the CASE_MANAGER, an Accept addressed to the Case Owner reaches an actor that cannot record the join, so the join is never recorded.
Refines: PCR-08-001
PCR-08-009 MUST The CASE_MANAGER MUST process an invitee's replies and record their effect on its own copy of the case: a reply to the stub Invite as CM-11-001 and CM-11-007 require, and a reply to the full-case Invite as CM-11-011 requires.

The invitee's replies are the only source of its join and its judgment of the case. If the CASE_MANAGER did not record them, the case would show the invitee as not joined, and the invitee would get no case content.
Refines: PCR-08-001
Refines: PCR-08-003
PCR-08-014 MUST NOT The Case Owner MUST NOT author or emit any RM state-change activity on behalf of the invitee.

RM state belongs to each participant, and it is the participant's own judgment (ADR-0121). Outside the bootstrap record of CBT-01-008, only that participant declares it. An RM change that the Case Owner emits for the invitee records a decision the invitee never made, and the case then shows the invitee engaged when it is not.
Refines: PCR-08-010
PCR-08-010 MUST NOT An actor processing a received activity MUST NOT construct or emit an activity whose actor is a different actor's ID (identity spoofing).

Receivers trust the actor field to name the actor that made the decision, and the sender checks depend on that. An activity that names another actor forges that actor's decision, and every receiver applies it as if that actor had sent it.
Refines: PCR-08-001
PCR-08-012 MUST The invitee's Accept(Invite(Actor)[target=VulnerabilityCaseStub]) is a join and embargo-consent decision, not an RM engagement decision. The CASE_MANAGER MUST NOT record any RM transition from it (CM-11-001), and MUST record the invitee as RM.ACCEPTED only on receiving Join(VulnerabilityCase) from it (CM-11-004).

PCR-08-014 prohibits the Case Owner from emitting RM state-change activities on behalf of the invitee. The invitee's own messages are the authoritative signals, and each carries only the decision it names: accepting the stub joins the case, the full-case Invite reply judges it (CM-11-011), and Join engages.
Refines: PCR-08-009

Case Bootstrap Trust

Requirements for establishing trust in a VulnerabilityCase and its CASE_MANAGER at non-owner sites. Governs the bootstrap process by which a non-owner participant verifies that an incoming case and its managing actor are legitimate before accepting case state updates.

Original Report-Submission Bootstrap

ID Priority Requirement Related
CBT-01-001 MUST When a case is created in response to an inbound Offer(VulnerabilityReport), the actor that received the report (the report receiver) MUST establish trust at the original report submitter by sending it, as itself, a one-time Create(VulnerabilityCase) activity. When the CASE_MANAGER is a different actor from the report receiver, that activity introduces the CASE_MANAGER the receiver delegated case maintenance to (ADR-0132).

The submitter chose the report receiver and wrote a record of that choice before the case existed. A case that arrives first from an actor the submitter never addressed gives it nothing to check the sender against, and a roster the sender wrote is not an anchor. The receiver already knows the CASE_MANAGER, so it vouches for it.
Refines: CM-12-001
Refines: PCR-02-005
CBT-01-002 MUST The bootstrap Create(VulnerabilityCase) activity MUST carry the complete VulnerabilityCase inline, rather than a reference, a minimal stub or a partial trust-only payload.

The receiver seeds its Participant Case Replica from this activity. A stub or a partial case leaves it nothing complete to seed from. A reference leaves it nothing to read: it has no trusted source to fetch the case from, because it trusts the CASE_MANAGER only after it accepts this activity (ADR-0170).
Implements: AKM-03-001
CBT-01-003 MUST The bootstrap case snapshot MUST distinguish the actor that asked for the case from the actor that created it (CBT-01-013, CBT-01-014).

The CASE_MANAGER creates the case (ADR-0041) and the report receiver asks for it, so the two roles can belong to two actors. CASE_OWNER is the standing of the party whose disclosure decisions the case serves, and it is never delegated. CASE_MANAGER is the right to write the canonical case ledger, held by the actor whose store holds the canonical case (ADR-0073). If the snapshot merged the roles, the ledger writer could also make the Case Owner's decisions.
Refines: CM-02-001
Refines: CM-02-019
Refines: PCR-01-002
CBT-01-013 MUST The bootstrap case snapshot MUST record the actor whose CaseProposal caused the case to exist as a participant with the CASE_OWNER role. That actor is the report receiver, whatever other roles it also holds.

CASE_OWNER is the standing of the party whose disclosure decisions the case serves. The report receiver asked for the case, so the snapshot names it as the Case Owner for every replica seeded from it.
Extends: CBT-01-003
CBT-01-014 MUST The bootstrap case snapshot MUST record the actor that created the case as a participant with the CASE_MANAGER role.

CASE_MANAGER is the right to write the canonical case ledger, held by the actor whose store holds the canonical case (ADR-0073). Every participant finds the CASE_MANAGER in the snapshot's participant records.
Extends: CBT-01-003
CBT-01-015 SHOULD In the bootstrap case snapshot, the participant that holds CASE_MANAGER SHOULD also carry the COORDINATOR role.

The actor that manages the case also coordinates it. Carrying the role lets checks that look for a coordinator find the CASE_MANAGER.
Extends: CBT-01-014
CBT-01-016 MUST NOT The CASE_OWNER role MUST NOT pass to the CASE_MANAGER by delegation.

CASE_OWNER is never delegated. If the snapshot merged the two roles by delegation, the ledger writer could also make the Case Owner's decisions.
Extends: CBT-01-003
CBT-01-004 MUST When the original report submitter is intended to receive ongoing case updates immediately, the bootstrap case snapshot MUST include that actor as a participant from the start.

The CASE_MANAGER shares case content only with active participants (CM-10-004). A submitter that is not on the starting roster gets no updates until it is invited, although it is the party that brought the report. On the roster from the start, it can follow the case from its first entry.
CBT-01-005 MUST NOT A report submitter MUST NOT trust a newly introduced CASE_MANAGER unless its bootstrap Create(VulnerabilityCase) comes from the report's receiver and references the submitted report.

The report receiver is an actor the submitter chose when it sent the report. The record the submitter wrote when it sent the report ties the report to that actor, so the sender cannot forge it (PCR-05-001). The sender writes the roster in the snapshot, so any actor can name itself CASE_MANAGER there. The report reference also stops an actor from attaching an unrelated case to the submitter.
Refines: PCR-05-001
Refines: AKM-02-001
CBT-01-017 MUST The report submitter MUST check the bootstrap of CBT-01-005 against a record it wrote itself before the case arrived, never against the snapshot's roster.

The sender writes the roster in the snapshot, so any actor can name itself CASE_MANAGER there. The record the submitter wrote when it sent the report ties the report to the actor it chose, so the sender cannot forge it (PCR-05-001).
Extends: CBT-01-005
CBT-01-006 MUST After validating the bootstrap Create(VulnerabilityCase), the receiving actor MUST persist the report-to-case link, the report receiver's identity (the actor that introduced the case), and the trusted CASE_MANAGER identity for that relationship.

The receiver checks later CASE_MANAGER traffic, such as Announce(VulnerabilityCase), against this record (CBT-02-001, CBT-02-003). Without it, the receiver could judge each later message only by what the message claims about itself. The report-to-case link also lets the submitter find the case for its report.
Refines: PCR-05-001
CBT-01-007 MUST The bootstrap Create(VulnerabilityCase) payload MUST carry each CaseParticipant record the case lists inline.

The receiver seeds its replica from this payload, and later guards read the participant records it seeds, such as the participant checks on a sender. A record the payload leaves out is one the replica lacks when a guard needs it. A record named only by reference is left out too: the receiver does not fetch it (AKM-05-007), because before it accepts this activity it has no trusted source to fetch it from (ADR-0170).
Refines: CBT-01-002
CBT-01-008 MUST The bootstrap case snapshot MUST include the original report submitter's participant record with an RM state of RM.ACCEPTED.

A report submitter can only be in RM.ACCEPTED at the time of submission: reporting is the act of accepting that the vulnerability warrants coordinated disclosure. A submitter who considered the item invalid would not have filed a report, and one who had deferred or closed their involvement would not be reporting at all. Because the CASE_MANAGER cannot observe the reporter's internal state progression, inserting intermediate RM records on the reporter's behalf would place unobserved events into the canonical case ledger. The CASE_MANAGER MUST instead reflect the only state the reporter could actually be in: RM.ACCEPTED.
Refines: CBT-01-004
Refines: SM-04-001
CBT-01-018 MUST The CASE_MANAGER MUST set the reporter's RM.ACCEPTED directly, never inserting intermediate transitions (RM.START, RM.RECEIVED, RM.VALID) it never observed for the reporter.

The CASE_MANAGER cannot observe the reporter's internal state progression. Intermediate RM records written on the reporter's behalf would place unobserved events into the canonical case ledger.
Extends: CBT-01-008
CBT-01-009 MUST The CASE_MANAGER MUST generate its Create(VulnerabilityCase) only after the VulnerabilityCase is fully initialized, so the activity records a completed initialization, not an intent. Fully initialized means every participant is at its correct initial RM state and every role is assigned.

ActivityStreams 2.0 activities represent completed events, not intentions. A Create(VulnerabilityCase) sent before the case is fully initialized would misrepresent the state that existed at creation time. Receivers replay the bootstrap snapshot as authoritative ledger content; if that snapshot is incomplete, every replica is permanently wrong. Sequencing the activity after initialization is the structural guarantee that the bootstrap snapshot is always accurate.
Refines: CBT-01-002
Refines: CBT-01-007
CBT-01-019 MUST The report receiver's introduction to the submitter (CBT-01-001) MUST relay the completed snapshot of CBT-01-009.

The submitter seeds its replica from the introduction. A relay that carried a different or earlier snapshot would seed the submitter's replica with a state the CASE_MANAGER never recorded.
Extends: CBT-01-009
CBT-01-010 MUST When the actor enacting CASE_MANAGER is not the report receiver, the CASE_MANAGER MUST NOT address its Create(VulnerabilityCase) to the original report submitter. That activity goes to the report receiver (the Case Owner), which relays the introduction (CBT-01-001). The CASE_MANAGER's first message to the submitter is an Announce. A submitter that receives it before the introduction holds it under CBT-03-001 and CBT-03-002.

The submitter never addressed the CASE_MANAGER, so a Create from it cannot be checked against anything the submitter wrote (CBT-01-005). Introducing the CASE_MANAGER through the actor the submitter chose keeps one trust anchor for the submitter and removes the route that trusted a sender because its own snapshot named it (ADR-0132).
Refines: CBT-01-001
Refines: CBT-01-005
CBT-01-011 MUST A participant MUST refuse a bootstrap Create(VulnerabilityCase) from a sender no trust anchor it wrote names as the expected sender, and store nothing from it. A snapshot naming that sender as CASE_MANAGER does not change this (CBT-01-017).

The sender wrote the snapshot, so a roster in it proves nothing about the sender's authority (ADR-0132; notes/participant-case-replica.md).
Refines: CBT-01-005

Post-Bootstrap Update Authority

ID Priority Requirement Related
CBT-02-001 MUST For the original report-submission path, Create(VulnerabilityCase) is a one-time bootstrap message only; subsequent full-case synchronization MUST use Announce(VulnerabilityCase) from the trusted CASE_MANAGER.

Two activity types let a receiver tell a first introduction from a later sync. It checks the Create against the record it wrote when it sent the report (CBT-01-005). It checks each Announce against the CASE_MANAGER that the accepted Create named (CBT-01-006).
Refines: PCR-02-001
Refines: PCR-03-001
CBT-02-002 MUST NOT Transport authentication or encrypted direct messaging alone MUST NOT be treated as establishing authority to trust a previously unknown CASE_MANAGER; the explicit bootstrap trust handoff is required.

Transport authentication shows which actor sent a message. It does not show that the actor may manage the case. Only a Trust Anchor the receiver wrote, from a relationship it had before the case arrived, ties the case to a CASE_MANAGER (CBT-01-005, CBT-04-001).
Refines: AKM-01-002
CBT-02-003 MUST A participant that trusts a CASE_MANAGER for a case MUST replace that identity only through an explicit transfer workflow, never through ordinary case-update traffic.

If ordinary case traffic could replace the trusted CASE_MANAGER, a case snapshot that names a different manager would move trust with no decision by the Case Owner. The receiver would then accept updates from an actor it never checked. An explicit workflow makes the change one step that participants can see.
Refines: CM-02-010

Out-of-Order Handling

ID Priority Requirement Related
CBT-03-001 SHOULD If a CASE_MANAGER-originated case message arrives before the required bootstrap trust handoff, the receiving actor SHOULD queue it only for a short bounded interval rather than accepting or permanently trusting it immediately.

The transport does not promise delivery order (SYNC-19-003), so a message from the real CASE_MANAGER can arrive before the bootstrap that makes it trusted. If the receiver drops the message at once, the message is lost. If the receiver applies it at once, it trusts a sender it has not checked. A short hold keeps the message until the bootstrap arrives, and the limit stops untrusted messages from staying in storage with no end.
Refines: PCR-06-002
CBT-03-002 MUST NOT A pre-bootstrap CASE_MANAGER message MUST NOT update local case state before the corresponding trust-establishing bootstrap message has been validated.

Before the bootstrap is validated, the receiver cannot know that the sender is the CASE_MANAGER. A state change from that message can come from any actor, and the receiver would then hold case state that no trusted source sent.
Refines: PCR-06-003
CBT-03-003 MUST When the bounded pre-bootstrap queue interval (CBT-03-001) expires, the receiver MUST drop the queued message.

A message the receiver cannot yet trust must not wait without limit for a case copy that may never arrive. Dropping it when the interval ends limits what the receiver stores. ADR-0163 gives the recovery of a dropped Invite to the CASE_MANAGER, which knows which Invites each participant has not answered.

Late-Joiner Bootstrap

ID Priority Requirement Related
CBT-04-001 MUST For a participant added after case creation, the trust-establishing message MUST be InviteActorToCase from the CASE_MANAGER rather than Create(VulnerabilityCase).

A late joiner submitted no report, so it holds no record that ties a case to an actor it chose. The stub Invite gives it one: the joiner chooses to accept it, and the accepted Invite is its Trust Anchor for the case (PCR-03-004). A Create(VulnerabilityCase) from an actor the joiner never dealt with gives it nothing to check the sender against.
Refines: CM-11-001
CBT-04-002 MUST The trust-establishing invite for a late joiner MUST identify both the case and the CASE_MANAGER that will send subsequent Announce(VulnerabilityCase) updates.

The joiner checks each later Announce(VulnerabilityCase) against the Trust Anchor the Invite left. If the Invite does not name both the case and the CASE_MANAGER, the joiner cannot tell which sender to trust for which case.
CBT-04-003 MUST After the late joiner has accepted the stub Invite (Accept(Invite(Actor)[target=VulnerabilityCaseStub])), the CASE_MANAGER MAY send Announce(VulnerabilityCase) without a separate Create(VulnerabilityCase) bootstrap.

The accepted stub Invite already gives the joiner its Trust Anchor (CBT-04-001). A further Create(VulnerabilityCase) adds a second trust step with nothing new to check, and it uses Create for a case the joiner did not see created.
Refines: PCR-07-007

Verification

ID Priority Requirement Related
CBT-05-008 MUST This rule covers a receiver that finds required participant data absent from a bootstrap Create(VulnerabilityCase) (CBT-01-007). It MUST reject the bootstrap as a protocol error, never skipping the participant, inferring its state, or synthesizing a record from data the payload lacks.

Silent reconstruction of missing protocol data masks protocol bugs: a fallback that fills in missing data "from domain knowledge" makes the protocol look as if it works when it does not, so the failure must be visible to be diagnosed and fixed. Replicas are built only from ledger content; data not in the ledger did not happen. A participant the payload names only by reference is absent too: CBT-01-007 requires it inline, and the receiver does not fetch it (AKM-05-007).
Implements: CBT-01-007
Implements: CBT-01-008

One Report Offered to Several Recipients

ID Priority Requirement Related
CBT-06-001 MUST Each Offer(VulnerabilityReport) a reporter sends MUST address exactly one recipient.

A reporter that gets no answer from its first recipient routinely offers the same report to a Coordinator, and either recipient may then accept and create its own case. The protocol permits this on purpose. The two attempts, and the cases that follow from them, are only distinguishable if each Offer names one recipient and has its own id.
Refines: HP-09-001
CBT-06-003 MUST Each Offer(VulnerabilityReport) a reporter sends MUST carry an activity id distinct from every other Offer of the same report.

With one recipient per Offer (CBT-06-001) and an id of its own, each submission attempt is identified by at least the pair (report, recipient). The cases that follow from two attempts can then be told apart.
Extends: CBT-06-001
CBT-06-002 MUST This rule covers a reporter that has offered one report to several recipients. It MUST accept a valid bootstrap Create(VulnerabilityCase) from each, checking each against its sender (CBT-01-005) and recording its case and CASE_MANAGER for that recipient alone.

A reporter often offers an unanswered report again to a Coordinator, and either recipient may then start a case. A reporter that trusts only one sender per report refuses the bootstrap from the second recipient and cannot join that case. How the reporter stores trust for each recipient is an implementation choice. This requirement controls only which bootstraps it accepts.
Refines: CBT-01-006
Depends on: CBT-01-005
CBT-06-004 MUST NOT This rule covers a reporter that has offered one report to several recipients. It MUST NOT let a further offer, or its acceptance of one bootstrap, change how it treats another recipient's bootstrap. That covers whether it accepts the bootstrap and which case it records for that recipient.

Each recipient's case stands on its own trust anchor. If one recipient's bootstrap could displace another's, the order in which the bootstraps arrive would decide which case the reporter can join.
Extends: CBT-06-002