Skip to main content

riffdb_application/
diff.rs

1use riffdb_types::{ApplicationRoleHash, CapabilityId};
2
3use crate::{
4    ApplicationInstallationPlan, CredentialDestination, InstallationArtifact, InstallationFeature,
5    InstallationPlanError, InstallationPlanErrorKind, InstallationRole, InstallationSymbol,
6    InstallationTarget, QuerySecretOutput, RoleAuthorityAtom, RoleAuthoritySet, RoleOperation,
7};
8
9/// Exact lifecycle observation required before any installation mutation.
10#[derive(Clone, Copy, Debug, Eq, PartialEq)]
11pub enum InstallationLifecycleState {
12    /// Selected database is authenticated, serving, and ready.
13    Ready,
14    /// Selected database is not ready; planning may describe but execution must stop.
15    NotReady,
16}
17
18/// Existing role state exposed symbolically for a bounded authority diff.
19#[derive(Clone, Debug, Eq, PartialEq)]
20pub struct ObservedInstallationRole {
21    name: InstallationSymbol,
22    role_hash: ApplicationRoleHash,
23    authority: RoleAuthoritySet,
24}
25
26impl ObservedInstallationRole {
27    /// Creates one sorted exact role observation.
28    pub fn new(
29        name: InstallationSymbol,
30        role_hash: ApplicationRoleHash,
31        operations: Vec<RoleOperation>,
32    ) -> Result<Self, InstallationPlanError> {
33        Self::new_with_authority(name, role_hash, RoleAuthoritySet::new(operations, vec![])?)
34    }
35
36    /// Creates one exact role observation over the complete closed authority vocabulary.
37    pub fn new_with_authority(
38        name: InstallationSymbol,
39        role_hash: ApplicationRoleHash,
40        authority: RoleAuthoritySet,
41    ) -> Result<Self, InstallationPlanError> {
42        Ok(Self {
43            name,
44            role_hash,
45            authority,
46        })
47    }
48
49    /// Symbolic role name.
50    #[must_use]
51    pub const fn name(&self) -> &InstallationSymbol {
52        &self.name
53    }
54
55    /// Exact current role identity.
56    #[must_use]
57    pub const fn role_hash(&self) -> ApplicationRoleHash {
58        self.role_hash
59    }
60
61    /// Current symbolic operation surface.
62    #[must_use]
63    pub fn operations(&self) -> &[RoleOperation] {
64        self.authority.operations()
65    }
66
67    /// Current symbolic named-query secret-output surface.
68    #[must_use]
69    pub fn secret_outputs(&self) -> &[QuerySecretOutput] {
70        self.authority.secret_outputs()
71    }
72
73    /// Complete current symbolic authority.
74    #[must_use]
75    pub const fn authority(&self) -> &RoleAuthoritySet {
76        &self.authority
77    }
78}
79
80/// Existing external credential slot identity without token material.
81#[derive(Clone, Debug, Eq, PartialEq)]
82pub struct ObservedCredentialDestination {
83    name: InstallationSymbol,
84    capability_id: CapabilityId,
85}
86
87impl ObservedCredentialDestination {
88    /// Creates one exact non-secret slot observation.
89    #[must_use]
90    pub const fn new(name: InstallationSymbol, capability_id: CapabilityId) -> Self {
91        Self {
92            name,
93            capability_id,
94        }
95    }
96
97    /// Symbolic slot name.
98    #[must_use]
99    pub const fn name(&self) -> &InstallationSymbol {
100        &self.name
101    }
102
103    /// Current capability identity, never bearer material.
104    #[must_use]
105    pub const fn capability_id(&self) -> CapabilityId {
106        self.capability_id
107    }
108}
109
110/// Bounded exact remote state used only for read-only planning.
111#[derive(Clone, Debug, Eq, PartialEq)]
112pub struct InstallationRemoteState {
113    target: InstallationTarget,
114    lifecycle: InstallationLifecycleState,
115    contract: Option<crate::InstallationContract>,
116    artifacts: Vec<InstallationArtifact>,
117    roles: Vec<ObservedInstallationRole>,
118    credentials: Vec<ObservedCredentialDestination>,
119    supported_features: Vec<InstallationFeature>,
120}
121
122impl InstallationRemoteState {
123    /// Validates and canonicalizes one bounded remote observation.
124    pub fn new(
125        target: InstallationTarget,
126        lifecycle: InstallationLifecycleState,
127        contract: Option<crate::InstallationContract>,
128        mut artifacts: Vec<InstallationArtifact>,
129        mut roles: Vec<ObservedInstallationRole>,
130        mut credentials: Vec<ObservedCredentialDestination>,
131        mut supported_features: Vec<InstallationFeature>,
132    ) -> Result<Self, InstallationPlanError> {
133        if artifacts.len() > crate::MAX_INSTALLATION_ARTIFACTS
134            || roles.len() > crate::MAX_INSTALLATION_ROLES
135            || credentials.len() > crate::MAX_CREDENTIAL_DESTINATIONS
136        {
137            return Err(InstallationPlanError::from_kind_for_diff(
138                InstallationPlanErrorKind::LimitExceeded,
139            ));
140        }
141        artifacts.sort();
142        reject_duplicate_keys(
143            artifacts
144                .windows(2)
145                .any(|pair| (pair[0].kind(), pair[0].name()) == (pair[1].kind(), pair[1].name())),
146        )?;
147        roles.sort_by(|left, right| left.name.cmp(&right.name));
148        reject_duplicate_keys(roles.windows(2).any(|pair| pair[0].name == pair[1].name))?;
149        credentials.sort_by(|left, right| left.name.cmp(&right.name));
150        reject_duplicate_keys(
151            credentials
152                .windows(2)
153                .any(|pair| pair[0].name == pair[1].name),
154        )?;
155        supported_features.sort();
156        reject_duplicate_keys(supported_features.windows(2).any(|pair| pair[0] == pair[1]))?;
157        Ok(Self {
158            target,
159            lifecycle,
160            contract,
161            artifacts,
162            roles,
163            credentials,
164            supported_features,
165        })
166    }
167
168    /// Exact selected target.
169    #[must_use]
170    pub const fn target(&self) -> &InstallationTarget {
171        &self.target
172    }
173
174    /// Current lifecycle/readiness state.
175    #[must_use]
176    pub const fn lifecycle(&self) -> InstallationLifecycleState {
177        self.lifecycle
178    }
179}
180
181/// Exact contract action required by a plan.
182#[derive(Clone, Copy, Debug, Eq, PartialEq)]
183pub enum InstallationContractAction {
184    /// Candidate is already active exactly.
185    AlreadyExact,
186    /// No contract is active; deploy the exact candidate.
187    DeployGenesis,
188    /// A different predecessor is active and the supplied exact migration must run.
189    ApplyMigration,
190    /// A different predecessor is active without an applicable exact migration.
191    StopForMigration,
192}
193
194/// Exact generated-artifact change.
195#[derive(Clone, Debug, Eq, PartialEq)]
196pub enum InstallationArtifactAction {
197    /// Artifact already exists at the exact identity.
198    AlreadyExact(InstallationArtifact),
199    /// Artifact is absent and must be published.
200    Publish(InstallationArtifact),
201    /// Same symbolic artifact exists at another immutable identity.
202    IdentityConflict {
203        /// Desired artifact.
204        desired: InstallationArtifact,
205        /// Observed inexact artifact.
206        observed: InstallationArtifact,
207    },
208}
209
210/// Bounded symbolic authority change for one role.
211#[derive(Clone, Debug, Eq, PartialEq)]
212pub struct InstallationRoleDiff {
213    name: InstallationSymbol,
214    current_hash: Option<ApplicationRoleHash>,
215    desired_hash: ApplicationRoleHash,
216    additions: Vec<RoleAuthorityAtom>,
217    removals: Vec<RoleAuthorityAtom>,
218    widening_approved: bool,
219}
220
221impl InstallationRoleDiff {
222    /// Symbolic role name.
223    #[must_use]
224    pub const fn name(&self) -> &InstallationSymbol {
225        &self.name
226    }
227
228    /// Current exact role identity, when one exists.
229    #[must_use]
230    pub const fn current_hash(&self) -> Option<ApplicationRoleHash> {
231        self.current_hash
232    }
233
234    /// Desired exact role identity.
235    #[must_use]
236    pub const fn desired_hash(&self) -> ApplicationRoleHash {
237        self.desired_hash
238    }
239
240    /// New symbolic authority.
241    #[must_use]
242    pub fn additions(&self) -> &[RoleAuthorityAtom] {
243        &self.additions
244    }
245
246    /// Removed symbolic authority.
247    #[must_use]
248    pub fn removals(&self) -> &[RoleAuthorityAtom] {
249        &self.removals
250    }
251
252    /// Whether the exact additions carry an accepted approval.
253    #[must_use]
254    pub const fn widening_approved(&self) -> bool {
255        self.widening_approved
256    }
257}
258
259/// Exact external credential action.
260#[derive(Clone, Debug, Eq, PartialEq)]
261pub enum InstallationCredentialAction {
262    /// Destination already contains the exact successor identity.
263    AlreadyExact(InstallationSymbol),
264    /// Empty destination may receive the successor.
265    Create(InstallationSymbol),
266    /// Exact predecessor may be replaced after successor proof.
267    Rotate(InstallationSymbol),
268    /// Destination contains an unapproved identity and must not be overwritten.
269    Occupied(InstallationSymbol),
270}
271
272/// Complete deterministic read-only diff against one selected database.
273#[derive(Clone, Debug, Eq, PartialEq)]
274pub struct ApplicationInstallationDiff {
275    lifecycle_ready: bool,
276    contract: InstallationContractAction,
277    artifacts: Vec<InstallationArtifactAction>,
278    roles: Vec<InstallationRoleDiff>,
279    credentials: Vec<InstallationCredentialAction>,
280    missing_features: Vec<InstallationFeature>,
281}
282
283impl ApplicationInstallationDiff {
284    /// Compiles one bounded diff without remote mutation.
285    pub fn compile(
286        plan: &ApplicationInstallationPlan,
287        remote: &InstallationRemoteState,
288    ) -> Result<Self, InstallationPlanError> {
289        let input = plan.input();
290        if &input.target != remote.target() {
291            return Err(InstallationPlanError::from_kind_for_diff(
292                InstallationPlanErrorKind::IdentityMismatch,
293            ));
294        }
295        let contract = match remote.contract {
296            None => InstallationContractAction::DeployGenesis,
297            Some(active) if active == input.contract => InstallationContractAction::AlreadyExact,
298            Some(active)
299                if input.migration.is_some_and(|migration| {
300                    migration.parent_version() == active.version()
301                        && migration.parent_bundle_hash() == active.bundle_hash()
302                }) =>
303            {
304                InstallationContractAction::ApplyMigration
305            }
306            Some(_) => InstallationContractAction::StopForMigration,
307        };
308        let artifacts = input
309            .artifacts
310            .iter()
311            .map(|desired| {
312                remote
313                    .artifacts
314                    .iter()
315                    .find(|observed| {
316                        observed.kind() == desired.kind() && observed.name() == desired.name()
317                    })
318                    .map_or_else(
319                        || InstallationArtifactAction::Publish(desired.clone()),
320                        |observed| {
321                            if observed == desired {
322                                InstallationArtifactAction::AlreadyExact(desired.clone())
323                            } else {
324                                InstallationArtifactAction::IdentityConflict {
325                                    desired: desired.clone(),
326                                    observed: observed.clone(),
327                                }
328                            }
329                        },
330                    )
331            })
332            .collect();
333        let roles = input
334            .roles
335            .iter()
336            .map(|desired| role_diff(desired, &remote.roles))
337            .collect();
338        let credentials = input
339            .credential_destinations
340            .iter()
341            .map(|desired| credential_diff(desired, &remote.credentials))
342            .collect();
343        let missing_features = input
344            .required_features
345            .iter()
346            .filter(|feature| remote.supported_features.binary_search(feature).is_err())
347            .copied()
348            .collect();
349        Ok(Self {
350            lifecycle_ready: remote.lifecycle == InstallationLifecycleState::Ready,
351            contract,
352            artifacts,
353            roles,
354            credentials,
355            missing_features,
356        })
357    }
358
359    /// Whether the selected database is ready.
360    #[must_use]
361    pub const fn lifecycle_ready(&self) -> bool {
362        self.lifecycle_ready
363    }
364
365    /// Required contract action.
366    #[must_use]
367    pub const fn contract(&self) -> InstallationContractAction {
368        self.contract
369    }
370
371    /// Exact artifact actions.
372    #[must_use]
373    pub fn artifacts(&self) -> &[InstallationArtifactAction] {
374        &self.artifacts
375    }
376
377    /// Symbolic role authority diffs.
378    #[must_use]
379    pub fn roles(&self) -> &[InstallationRoleDiff] {
380        &self.roles
381    }
382
383    /// External credential actions.
384    #[must_use]
385    pub fn credentials(&self) -> &[InstallationCredentialAction] {
386        &self.credentials
387    }
388
389    /// Required features absent from the exact server/driver observation.
390    #[must_use]
391    pub fn missing_features(&self) -> &[InstallationFeature] {
392        &self.missing_features
393    }
394
395    /// Whether execution may start without operator repair or new approval.
396    #[must_use]
397    pub fn executable(&self) -> bool {
398        self.lifecycle_ready
399            && self.contract != InstallationContractAction::StopForMigration
400            && self.missing_features.is_empty()
401            && self.roles.iter().all(|role| {
402                role.additions.is_empty() || role.current_hash.is_none() || role.widening_approved
403            })
404            && self
405                .credentials
406                .iter()
407                .all(|action| !matches!(action, InstallationCredentialAction::Occupied(_)))
408            && self.artifacts.iter().all(|action| {
409                !matches!(action, InstallationArtifactAction::IdentityConflict { .. })
410            })
411    }
412}
413
414fn role_diff(
415    desired: &InstallationRole,
416    observed_roles: &[ObservedInstallationRole],
417) -> InstallationRoleDiff {
418    let observed = observed_roles
419        .iter()
420        .find(|observed| observed.name == *desired.name());
421    let existing = observed.map_or_else(Vec::new, |role| role.authority.atoms());
422    let desired_atoms = desired.desired_authority().atoms();
423    let additions = desired_atoms
424        .iter()
425        .filter(|atom| existing.binary_search(atom).is_err())
426        .cloned()
427        .collect::<Vec<_>>();
428    let removals = existing
429        .iter()
430        .filter(|atom| desired_atoms.binary_search(atom).is_err())
431        .cloned()
432        .collect::<Vec<_>>();
433    let widening_approved = additions.is_empty()
434        || observed.is_none()
435        || desired.widening_approval().is_some_and(|approval| {
436            observed.is_some_and(|role| approval.expected_previous_role() == role.role_hash)
437                && approval.additions() == additions
438        });
439    InstallationRoleDiff {
440        name: desired.name().clone(),
441        current_hash: observed.map(ObservedInstallationRole::role_hash),
442        desired_hash: desired.role_hash(),
443        additions,
444        removals,
445        widening_approved,
446    }
447}
448
449fn credential_diff(
450    desired: &CredentialDestination,
451    observed_credentials: &[ObservedCredentialDestination],
452) -> InstallationCredentialAction {
453    let observed = observed_credentials
454        .iter()
455        .find(|observed| observed.name == *desired.name());
456    match observed {
457        Some(observed) if observed.capability_id == desired.successor() => {
458            InstallationCredentialAction::AlreadyExact(desired.name().clone())
459        }
460        None if desired.expected_current().is_none() => {
461            InstallationCredentialAction::Create(desired.name().clone())
462        }
463        Some(observed) if Some(observed.capability_id) == desired.expected_current() => {
464            InstallationCredentialAction::Rotate(desired.name().clone())
465        }
466        _ => InstallationCredentialAction::Occupied(desired.name().clone()),
467    }
468}
469
470fn reject_duplicate_keys(duplicate: bool) -> Result<(), InstallationPlanError> {
471    if duplicate {
472        Err(InstallationPlanError::from_kind_for_diff(
473            InstallationPlanErrorKind::Duplicate,
474        ))
475    } else {
476        Ok(())
477    }
478}