Skip to content

Commit 34f75f0

Browse files
feat(engine): Engine::reconcile_commit and probe_manual_drift (#70)
* feat(state): extend ReconciliationEntry with adopted/skipped vectors Adds two `Vec<String>` fields to `pearlite_state::ReconciliationEntry`, both `#[serde(default)]` so pre-ADR `[[reconciliations]]` rows deserialize cleanly with empty vectors. Tightens `package_count`'s docstring to name it as the audit denominator (ADR-0014 §9). Backfills three round-trip tests in `reconciliation::tests`: - legacy entry without decision vectors deserializes with empty `[]`, - populated decision vectors round-trip through TOML, - AdoptAll with empty `skipped` round-trips. Schema change is additive; the `ReconciliationAction` unit-variant enum (AdoptAll / Interactive / Skipped) keeps its existing TOML representation per ADR-0014 §7. Refs: ADR-0014, PRD §7.3 Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(engine): Engine::reconcile_commit and probe_manual_drift Adds the write-side of reconcile (ADR-0014, M4 W1): - `Engine::reconcile_commit(state_path, &decisions, threshold)` — probes via the existing adapter, classifies Manual drift through `pearlite_diff::{classify_pacman, classify_cargo}` against an empty declared `PackageSet` (the fresh-import path from PRD §11), enforces the `Some(N)` threshold defensively, unions adopted names into `state.adopted.{pacman,cargo}`, and appends one `[[reconciliations]]` row with a fresh `Uuid::now_v7()`. Atomic write via `StateStore`; `state.last_modified` set, `state.last_apply` deliberately untouched. - `Engine::probe_manual_drift(state_path)` — read-only helper that returns the merged sorted-deduplicated Manual list. The CLI uses it to drive the threshold pre-check and per-package prompt loop without reaching into the engine's private probe accessor. - `ReconcileDecisions` enum (`AdoptAll` / `Selective { adopt }`) and `ReconcileCommitOutcome` mirror the ADR §7 split: enum carries the *policy*, vectors carry the *decisions*. - `ReconcileCommitError::{Probe, State, ThresholdExceeded}` for the three failure modes; threshold variant carries `count` and `threshold` so the CLI can surface ADR-0014 §2 wording verbatim. Module doc now describes both reconcile entry points (read-only import + commit). Ten new tests in `reconcile::tests`: AdoptAll happy path, Selective partition, threshold exceeded refuses without writing, threshold boundary allowed, `state.managed` packages stay classified as Forgotten (not adopted), existing `state.adopted` preserved on union, no-Manual-drift still records empty entry, probe / state-read failure propagation, fresh `plan_id` per call. Refs: ADR-0014, PRD §11 Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 0c79680 commit 34f75f0

3 files changed

Lines changed: 610 additions & 21 deletions

File tree

crates/pearlite-engine/src/errors.rs

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,35 @@ pub enum ReconcileError {
172172
},
173173
}
174174

175+
/// Errors emitted by
176+
/// [`Engine::reconcile_commit`](crate::Engine::reconcile_commit) (ADR-0014,
177+
/// M4 W1).
178+
///
179+
/// `reconcile_commit` resolves the Manual drift surfaced by reconcile by
180+
/// promoting items into `state.adopted` and recording a
181+
/// `[[reconciliations]]` entry. Variants identify whether the failure was
182+
/// on the probe side, the state read/write side, or the threshold-guard.
183+
#[derive(Debug, Error)]
184+
pub enum ReconcileCommitError {
185+
/// Probing the live system failed.
186+
#[error(transparent)]
187+
Probe(#[from] ProbeError),
188+
/// `state.toml` read or write failed.
189+
#[error(transparent)]
190+
State(#[from] pearlite_state::StateError),
191+
/// Engine-side threshold guard: the count of Manual drift items
192+
/// exceeds the supplied limit. Defense-in-depth — the CLI normally
193+
/// aborts with `RECONCILE_THRESHOLD_EXCEEDED` before the engine
194+
/// runs (ADR-0014 §2).
195+
#[error("reconcile-commit refused: {count} Manual items exceeds threshold {threshold}")]
196+
ThresholdExceeded {
197+
/// Number of Manual drift items found.
198+
count: u32,
199+
/// Threshold the caller asked the engine to enforce.
200+
threshold: u32,
201+
},
202+
}
203+
175204
/// Errors emitted by [`Engine::plan`](crate::Engine::plan) and friends.
176205
#[derive(Debug, Error)]
177206
pub enum EngineError {

crates/pearlite-engine/src/lib.rs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,12 @@ mod rollback;
1818
pub use apply::{ApplyContext, ApplyOutcome, FailureRecord};
1919
pub use bootstrap::BootstrapOutcome;
2020
pub use errors::{
21-
ApplyError, BootstrapError, EngineError, ProbeError, ReconcileError, RollbackError,
21+
ApplyError, BootstrapError, EngineError, ProbeError, ReconcileCommitError, ReconcileError,
22+
RollbackError,
2223
};
2324
pub use plan::Engine;
2425
pub use probe::{LiveProbe, SystemProbe};
25-
pub use reconcile::ReconcileOutcome;
26+
pub use reconcile::{ReconcileCommitOutcome, ReconcileDecisions, ReconcileOutcome};
2627
pub use rollback::RollbackOutcome;
2728

2829
#[cfg(any(test, feature = "test-mocks"))]

0 commit comments

Comments
 (0)