CaseLedger Causal Ordering: CaseActor Observation Order Is the Canonical Causal Order
Context and Problem Statement
notes/case-ledger-authority.md states informally that "log_index order
is causal order", a claim that was confirmed in the native-entry section of
that note and again when WritePrologueLedgerEntriesNode was removed
(Issue #1777). DEMOMA-22-005 relies on the same property, citing ADR-0041
as its basis. ADR-0041 establishes CaseActor-authoritative initialization
— it does not establish a normative causal-ordering model.
This leaves DEMOMA-22-005 standing on an unwritten foundation. The property is real and intended; what is missing is the decision record. Three related pieces need to be unified as normative requirements:
- The CaseActor's obligation:
log_indexorder is causal order from the CaseActor's own perspective. - Testable temporal invariants: constraints on
CaseLedgerEntry.publishedandlog_indexthat conformance tests can verify. - Participant emission obligations: what a well-behaved participant must do to preserve causal fidelity in the assertions it submits to the CaseActor.
Decision Drivers
- DEMOMA-22-005 requires that invariant tests check declared causal edges against observed ledger order, but "observed ledger order is causal order" is not itself a normative spec entry.
- The ledger is the replicated source of truth for all participants; the ordering model must be unambiguous.
- The CaseActor is a single writer (ADR-0041, CLP-01-003), which uniquely positions it to enforce a deterministic ordering model without distributed consensus overhead.
- Residual uncertainty about external actor clocks must be explicitly acknowledged rather than papered over.
Considered Options
A. CaseActor observation order — the log_index sequence assigned by the
CaseActor's single authoritative write path defines causal order. For
internally-generated events, order is known by construction. For inbound
participant assertions, the CaseActor's receipt and commit order is
authoritative. The CaseActor's own published timestamp stamps the ledger
envelope; the participant's claimed timestamp is preserved inside the
payloadSnapshot but does not determine ledger order.
B. Vector clock causal tracking — each activity carries a logical clock vector across all known participants. The ledger records vector values; receivers can reconstruct a partial causal order across participants without routing everything through the CaseActor.
C. Pure wall-clock ordering — entries are ordered by their published
timestamp alone. Log index is a convenience counter; the authoritative order
is timestamp sort order.
Decision Outcome
Chosen option: A, CaseActor observation order, because:
- The CaseActor is already the single writer (ADR-0041). There is no concurrent-write hazard to model.
- Option B requires propagating vector clocks through every wire activity. The federated protocol has no guarantee that all participants maintain compatible clock vectors, and the complexity cost is not justified when a single-writer regime already resolves the ordering problem.
- Option C is unreliable: clock skew between the CaseActor's clock and participants' clocks means physically simultaneous events can receive arbitrary timestamp order. Timestamps are useful supporting evidence but must not be the sole determinant.
The Ordering Model
The CaseLedger is authoritative from the CaseActor's perspective only
— a postal cancellation stamp, not a forensic record. The CaseActor records
the sequence in which it observed and processed events. It makes no claim
about when events occurred at other actors.
- For internal events generated by the CaseActor itself (e.g., the
native case-initialization entries in ADR-0041), causal order is known by
construction: each entry is committed sequentially in a single execution
path, so the resulting
log_indexsequence is the causal sequence. - For external events (inbound participant assertions), the CaseActor's receipt and commit order is the authoritative ordering. The CaseActor cannot verify causal claims about events it did not observe.
CaseActor Timestamp Obligation
The CaseActor MUST ensure:
- If event A causally precedes event B (from the CaseActor's perspective),
then
log_index(B) > log_index(A). - The
log_indexsequence is gapless. The genesis entry carries index 0 and each later entry carries its predecessor's index plus one (CLP-14-010, SYNC-01-002). A receiver that gets an entry that does not extend its chain and has an index greater thantail_index + 1concludes an entry is missing (SYNC-14-001). That conclusion holds only because the CaseActor never skips an index. - The
publishedtimestamps on consecutive ledger entries MUST be monotonically non-decreasing inlog_indexorder. An entry stamped later in the sequence MUST NOT carry an earlier timestamp than its predecessor. - Every ledger entry's
publishedtimestamp MUST be on or after the case's ownpublishedtimestamp. A ledger entry cannot predate the case it belongs to.
This is not a claim that B immediately follows A. Other entries may be recorded between them. The claim is only that B cannot appear before A.
Participant Emission Obligation
Participants MUST emit protocol messages in causal order, with timestamps
that reflect the actual sequence of events. A participant MUST NOT batch
events and emit them in arbitrary order. If A caused B caused C, the
participant MUST send them in that sequence with monotonically non-decreasing
published timestamps. A participant that violates this produces a malformed
assertion — that is a protocol violation on the participant's part, not a
CaseActor failure.
Residual Uncertainty
Even with the participant obligation above, the CaseActor cannot verify causal claims from external actors. The ledger is authoritative for what the CaseActor observed; it is not a guarantee of real-world causal fidelity when external actors self-report events. The CaseActor SHOULD reject payload assertions with timestamps that are implausibly far in the future or past relative to its own clock, but it cannot eliminate all timestamp spoofing from a malicious or malfunctioning participant.
Consequences
- Good: the
log_indexsequence is the unambiguous causal order. Every conformance invariant that references causal order can be evaluated againstlog_indexwithout clock comparison. - Good:
notes/case-ledger-authority.md's informal claim is now grounded in a normative decision record that DEMOMA-22-005 can cite. - Good: the residual uncertainty about external actor clocks is explicitly acknowledged, rather than being an implicit gap in the spec.
- Neutral: participants bear an obligation the CaseActor can only partially enforce (timestamp ordering is checkable; causal ordering across opaque participant-internal events is not).
- Bad: a participant whose clock is wrong produces a structurally valid but temporally misleading assertion. The CaseActor can flag implausible timestamps (SHOULD-level) but cannot always determine the ground truth.
Validation
Enforcement splits along the two timestamps an entry carries, and the split is load-bearing — conflating them produces either a vacuous check or a false rejection (Issue #2824).
Commit timestamp (CaseLedgerEntry.published, stamped by the CaseActor).
check_clp14_timestamp_invariants in test/ci/invariants/common.py, run by
every scenario through universal_harness.py, verifies:
entry.publishedis non-null for all entries.entry[N].published >= entry[N-1].publishedfor all consecutive recorded entries within a case.log_indexvalues are unique within a case (CLP-14-005) and form a gapless sequence from 0 (CLP-14-010).- All entries in a case share the same
case_id. - No entry's
publishedprecedes the case-creation entry's timestamp.
A single writer stamping from one clock satisfies these by construction; the harness is what catches a replica that does not.
Claimed timestamp (payloadSnapshot.published, chosen by the asserting
actor). Its presence is guaranteed upstream: parse_activity refuses an inbound
activity that carries no published (CLP-15-006), because ActivityStreams makes
the field optional and the wire base class defaults it to now_utc — so a
receiver that let it through could no longer tell a claimed time from its own
clock, which is exactly how CLP-14-007 and CLP-14-008 were left vacuous
(Issue #3149).
_validate_entry_timestamps, reached from every commit through
CreateLogEntryNode, then refuses an assertion that:
- predates the case by more than the configured clock-skew tolerance (CLP-14-006);
- sits beyond the future tolerance or staleness window (CLP-14-007, CLP-14-008).
and reports without refusing an assertion whose claimed timestamp regresses
behind that same actor's previous assertion (CLP-15-003) — at INFO within the
clock-skew tolerance, at WARNING beyond it.
That asymmetry is the load-bearing part. CLP-15-003 binds the participant, and the CaseActor observes arrival order, not causal order: the transport gives no ordering guarantee (ADR-0037), so two assertions from one actor can legitimately be committed in the reverse of the order they were emitted, and CLP-15-003 constrains only assertions that are actually causally related. Refusing on that signal would be the reconstruction CLP-15-005 forbids — and it would discard the assertion outright, because the guarded commit precedes the effect nodes (CLP-10-006), leaving no ledger record and no retry. Recording it and reporting it keeps the signal without losing the assertion.
Monotonicity of the claimed timestamp is scoped to one actor's stream. Applying
it across actors would reintroduce option C: two participants' clocks are not
comparable, so a later-committed assertion from one legitimately carries an
earlier claimed time than an earlier-committed one from another. For the same
reason a snapshot the CaseActor builds on a participant's behalf must carry that
participant's claimed time, not the CaseActor's clock — mixing the two inside one
actor's stream manufactures a regression. Thresholds are tunable via
AppConfig.ledger (CLP-14-009).
The participant emission obligations CLP-15-001 and CLP-15-002 are not
enforceable per-assertion — that is the point of the residual uncertainty above,
and CLP-15-005 forbids the attempt. They are validated at the conformance layer
by check_causal_edges, which compares declared causal edges against observed
ledger order across a whole scenario (DEMOMA-22-005).
More Information
- Source concern: #2334
DEMOMA-22-005now cites this ADR instead of ADR-0041.notes/case-ledger-authority.mdinformally anticipated this decision; that note now cross-references this ADR.- Issue #2752 corrected the ordering model. It had said gaps in
log_indexwere permitted, which contradicted SYNC-01-002 and the Validation section. CLP-14-010 now states the gapless rule. - Issue #2207 (ADR-0041 mis-citation) can be fixed once this ADR and the generated spec entries (CLP-14, CLP-15) are in place.
Generated spec requirements: specs/case-ledger-processing.yaml CLP-14-001
through CLP-14-010, CLP-15-001 through CLP-15-005.