Skip to content

0019. Record two deletion causes

Date: 2026-09-08 Status: Accepted

Context

ADR-0016 decided that a deletion propagates according to its recorded cause and never according to the delivery policy alone, and it named three causes: an explicit statement from the source, an absence inferred from a full sync, and a teardown of Conflux's own mirror. The middle one carried the strictest rule, propagating only where a rule opts in, because a truncated response and a real deletion look identical from Conflux's side.

That taxonomy described the system as it stood. CFX-211 then removed the reconciliation that inferred deletions from absence, and made the deletion log the sole mechanism by which a source deletion is detected. The reasoning is on the Deletion Log Sync page: absence in a delta sync means "unchanged" rather than "deleted", a comparison against the local set therefore only works after a full sync, and full syncs run on demand rather than on a schedule. Absence inference was rejected, not postponed.

CFX-212 is the ticket that writes the cause. It therefore has to decide which causes exist before CFX-217 builds a policy on them.

Decision

A deletion has two recordable causes. source_event means the source declared the record gone, which today means the deletion log reported it. connection_lifecycle means a disconnect or an uninstall dropped rows from Conflux's mirror.

A cause of reconciled_absent is not recorded, because nothing can produce one. Keeping the value would give the policy in CFX-217 a branch that can never be exercised and a test that proves nothing about the running system. If a future integration arrives without a deletion log, absence inference would have to be decided again on its own merits, and it would come with a new cause and a new ADR.

A null cause is valid and means the cause was not recorded. Every row soft-deleted before CFX-212 carries null, and those rows are deliberately not backfilled: the cause was never stored and any value written now would be a guess presented as a fact. A tombstone without a cause is ordinary, which App\Data\Tombstone already states. The propagation policy treats null the way it treats connection_lifecycle, which is the conservative reading: a deletion Conflux cannot explain does not reach somebody else's live system.

Only source_event moves content_changed_at. The delivery cursor is ordered by that column, so moving it is what makes a tombstone reachable. A source deletion changed the record and moves it. A lifecycle teardown did not, and moving it there would make every row of a disconnected connection deliverable in a single sweep, which the policy would then have to filter out again.

Consequences

The three-value table in Sync Rules and the matching paragraph in Outbound Delivery become two-value, plus the null rule.

CFX-217 implements two policy branches and a null branch rather than three branches.

A deletion Conflux infers rather than reads has no representation. That is the intended state: the inference does not exist, and inventing a cause for it would be the more dangerous outcome.