package domain import ( "errors" "fmt" rvboxv1 "github.com/rvbox/rvbox/gen/go/rvbox/v1" ) var ErrReconcileContradiction = errors.New("reconciliation evidence contradicts durable state") type ClientEvidenceKind uint8 const ( ClientEvidenceAbsent ClientEvidenceKind = iota ClientEvidenceRetained ClientEvidenceTombstone ) type ReconcileAction uint8 const ( ReconcileNoop ReconcileAction = iota ReconcileRequeue ReconcileResumeDelivery ReconcileInterruptServerSuppressReplay ReconcileInterruptServerClientStateLoss ReconcileTerminateLocal ReconcileDiscardLocalTerminal ReconcileRecordContradiction ) type ReconcileInput struct { ServerPresent bool ServerLifecycle rvboxv1.CommandLifecycle ServerRevision CommandRevision ServerLastEventSeq uint64 ServerHasTombstone bool ClientEvidence ClientEvidenceKind ClientLifecycle rvboxv1.CommandLifecycle ClientRevision CommandRevision ClientLastEventSeq uint64 ImmutableHashMatches bool } type ReconcileDecision struct { Action ReconcileAction EffectiveRevision CommandRevision RecordIncident bool SuppressReplay bool } // DecideReconciliation implements the explicit server/client evidence matrix. // Persistence and message emission remain the caller's atomic responsibility. func DecideReconciliation(input ReconcileInput) (ReconcileDecision, error) { if input.ClientEvidence > ClientEvidenceTombstone { return ReconcileDecision{}, fmt.Errorf("%w: unknown client evidence", ErrReconcileContradiction) } if input.ServerPresent { if input.ServerLifecycle == rvboxv1.CommandLifecycle_COMMAND_LIFECYCLE_UNSPECIFIED || input.ServerRevision == 0 { return ReconcileDecision{}, fmt.Errorf("%w: invalid server row", ErrReconcileContradiction) } } if input.ClientEvidence != ClientEvidenceAbsent { if input.ClientRevision == 0 || !input.ImmutableHashMatches { return ReconcileDecision{}, fmt.Errorf("%w: invalid revision or immutable hash", ErrReconcileContradiction) } } if !input.ServerPresent { if input.ClientEvidence == ClientEvidenceAbsent { return ReconcileDecision{Action: ReconcileNoop}, nil } if input.ClientEvidence == ClientEvidenceTombstone || IsTerminal(input.ClientLifecycle) { return ReconcileDecision{Action: ReconcileDiscardLocalTerminal, EffectiveRevision: input.ClientRevision, SuppressReplay: true}, nil } if input.ServerHasTombstone { return ReconcileDecision{Action: ReconcileTerminateLocal, EffectiveRevision: input.ClientRevision, RecordIncident: true, SuppressReplay: true}, nil } return ReconcileDecision{Action: ReconcileTerminateLocal, EffectiveRevision: input.ClientRevision, RecordIncident: true, SuppressReplay: true}, nil } if IsTerminal(input.ServerLifecycle) { if input.ClientEvidence == ClientEvidenceAbsent { return ReconcileDecision{Action: ReconcileNoop, EffectiveRevision: input.ServerRevision}, nil } if input.ClientEvidence == ClientEvidenceTombstone || IsTerminal(input.ClientLifecycle) { if input.ClientEvidence == ClientEvidenceRetained && input.ClientLifecycle != input.ServerLifecycle { return ReconcileDecision{Action: ReconcileRecordContradiction, EffectiveRevision: maxRevision(input.ServerRevision, input.ClientRevision), RecordIncident: true, SuppressReplay: true}, fmt.Errorf("%w: terminal lifecycle mismatch", ErrReconcileContradiction) } return ReconcileDecision{Action: ReconcileDiscardLocalTerminal, EffectiveRevision: maxRevision(input.ServerRevision, input.ClientRevision), SuppressReplay: true}, nil } return ReconcileDecision{Action: ReconcileTerminateLocal, EffectiveRevision: maxRevision(input.ServerRevision, input.ClientRevision), RecordIncident: true, SuppressReplay: true}, nil } if input.ClientEvidence == ClientEvidenceTombstone { return ReconcileDecision{Action: ReconcileInterruptServerSuppressReplay, EffectiveRevision: maxRevision(input.ServerRevision, input.ClientRevision), RecordIncident: true, SuppressReplay: true}, nil } if input.ClientEvidence == ClientEvidenceAbsent { switch input.ServerLifecycle { case rvboxv1.CommandLifecycle_COMMAND_QUEUED, rvboxv1.CommandLifecycle_COMMAND_DISPATCHED: return ReconcileDecision{Action: ReconcileRequeue, EffectiveRevision: input.ServerRevision}, nil case rvboxv1.CommandLifecycle_COMMAND_ACCEPTED, rvboxv1.CommandLifecycle_COMMAND_RUNNING: return ReconcileDecision{Action: ReconcileInterruptServerClientStateLoss, EffectiveRevision: input.ServerRevision, RecordIncident: true, SuppressReplay: true}, nil default: return ReconcileDecision{}, fmt.Errorf("%w: unsupported non-terminal server state", ErrReconcileContradiction) } } if input.ClientLifecycle == rvboxv1.CommandLifecycle_COMMAND_LIFECYCLE_UNSPECIFIED { return ReconcileDecision{}, fmt.Errorf("%w: invalid client lifecycle", ErrReconcileContradiction) } if input.ClientLastEventSeq < input.ServerLastEventSeq { return ReconcileDecision{Action: ReconcileRecordContradiction, EffectiveRevision: maxRevision(input.ServerRevision, input.ClientRevision), RecordIncident: true, SuppressReplay: true}, fmt.Errorf("%w: client lost server-confirmed events", ErrReconcileContradiction) } if input.ServerLifecycle == rvboxv1.CommandLifecycle_COMMAND_QUEUED { return ReconcileDecision{Action: ReconcileRecordContradiction, EffectiveRevision: maxRevision(input.ServerRevision, input.ClientRevision), RecordIncident: true, SuppressReplay: true}, fmt.Errorf("%w: queued server command exists at client", ErrReconcileContradiction) } return ReconcileDecision{Action: ReconcileResumeDelivery, EffectiveRevision: maxRevision(input.ServerRevision, input.ClientRevision)}, nil } func maxRevision(left, right CommandRevision) CommandRevision { if left > right { return left } return right }