magi-code 0.96.1

Repository-aware CLI coding agent for terminal work
Documentation
use super::*;

pub(super) fn changes_from_records(store: &CheckpointStore, read: LedgerRead) -> ChangesReadModel {
    let mut by_turn: BTreeMap<u64, Vec<ChangeEntry>> = BTreeMap::new();
    for record in read.records {
        let event = record.event;
        let visible_path = redacted_relative_path(&event.status, event.relative_path.clone());
        let (classification, rewindable, unavailable_reason) = match event.status {
            SnapshotCaptureStatus::Captured => {
                let available = event
                    .pre
                    .as_ref()
                    .into_iter()
                    .chain(event.post.as_ref())
                    .all(|blob| store.blob_path(&blob.sha256).exists());
                let classification = match (&event.pre, &event.post) {
                    (None, Some(_)) => ChangeClassification::Created,
                    (Some(_), Some(_)) => ChangeClassification::Modified,
                    (Some(_), None) => ChangeClassification::Deleted,
                    (None, None) => ChangeClassification::NonRewindable,
                };
                (
                    classification,
                    available,
                    (!available).then_some("missing checkpoint blob".to_string()),
                )
            }
            SnapshotCaptureStatus::Excluded { reason } => (
                ChangeClassification::NonRewindable,
                false,
                Some(format!("excluded: {reason:?}")),
            ),
            SnapshotCaptureStatus::Oversized { bytes, max_bytes } => (
                ChangeClassification::NonRewindable,
                false,
                Some(format!("oversized: {bytes} > {max_bytes}")),
            ),
            SnapshotCaptureStatus::Unavailable { reason } => {
                (ChangeClassification::NonRewindable, false, Some(reason))
            }
        };
        by_turn
            .entry(event.user_turn)
            .or_default()
            .push(ChangeEntry {
                tool: event.tool,
                relative_path: visible_path,
                classification,
                rewindable,
                unavailable_reason,
            });
    }
    ChangesReadModel {
        turns: by_turn
            .into_iter()
            .map(|(user_turn, mut changes)| {
                changes.sort_by(|left, right| left.relative_path.cmp(&right.relative_path));
                ChangeTurn { user_turn, changes }
            })
            .collect(),
        diagnostics: read.diagnostics,
    }
}

pub(super) fn plan_rewind(
    store: &CheckpointStore,
    session_id: &str,
    cwd: &Path,
    target_turn: u64,
) -> RestorePlan {
    let read = store.read_records(session_id);
    plan_rewind_from_records(store, session_id, cwd, target_turn, &read)
}

pub(super) fn plan_rewind_from_records(
    store: &CheckpointStore,
    session_id: &str,
    cwd: &Path,
    target_turn: u64,
    read: &LedgerRead,
) -> RestorePlan {
    let latest_turn = read
        .records
        .iter()
        .map(|record| record.event.user_turn)
        .max();
    let selected = read
        .records
        .iter()
        .filter(|record| record.event.user_turn >= target_turn)
        .cloned()
        .collect::<Vec<_>>();
    let mut by_path: BTreeMap<PathBuf, Vec<SnapshotLedgerRecord>> = BTreeMap::new();
    for record in selected {
        by_path
            .entry(record.event.relative_path.clone())
            .or_default()
            .push(record);
    }

    // The ambient open is the trusted boundary. Every target below is then
    // resolved relative to this held directory, never through `cwd` again.
    let (canonical_cwd, root) = open_rewind_root(cwd);
    let mut operations = Vec::new();
    for (relative_path, mut records) in by_path {
        records.sort_by_key(|record| record.event.user_turn);
        if let Err(reason) = safe_rewind_target_path(store, &canonical_cwd, &relative_path) {
            operations.push(RestoreOperation {
                relative_path: PathBuf::from(REDACTED_PATH),
                kind: RestoreOperationKind::SkipUnavailable {
                    reason: reason.to_string(),
                },
            });
            continue;
        }

        let first = &records.first().expect("group non-empty").event;
        let last = &records.last().expect("group non-empty").event;
        if !records
            .iter()
            .all(|record| record.event.status == SnapshotCaptureStatus::Captured)
        {
            operations.push(RestoreOperation {
                relative_path,
                kind: RestoreOperationKind::SkipUnavailable {
                    reason: "non-rewindable snapshot".to_string(),
                },
            });
            continue;
        }

        let expected_current = last.post.clone();
        let current = match root.as_ref() {
            Some(root) => {
                match target_snapshot_for_plan(root, &relative_path, expected_current.is_none()) {
                    Ok(current) => current,
                    Err(_) => {
                        operations.push(RestoreOperation {
                            relative_path,
                            kind: RestoreOperationKind::SkipUnavailable {
                                reason: "hash check failed".to_string(),
                            },
                        });
                        continue;
                    }
                }
            }
            None => {
                operations.push(RestoreOperation {
                    relative_path,
                    kind: RestoreOperationKind::SkipUnavailable {
                        reason: "hash check failed".to_string(),
                    },
                });
                continue;
            }
        };
        if let Some(expected) = expected_current.as_ref() {
            if current.as_ref().map(|snapshot| snapshot.hash.as_str())
                != Some(expected.sha256.as_str())
            {
                operations.push(RestoreOperation {
                    relative_path,
                    kind: RestoreOperationKind::SkipConflict {
                        reason: "current file hash differs from checkpoint post-image".to_string(),
                    },
                });
                continue;
            }
        } else if current.is_some() {
            operations.push(RestoreOperation {
                relative_path,
                kind: RestoreOperationKind::SkipConflict {
                    reason: "expected deleted file exists".to_string(),
                },
            });
            continue;
        }

        let kind = match (&first.pre, expected_current) {
            (Some(pre), Some(expected_current)) => {
                if !store.blob_path(&pre.sha256).exists() {
                    RestoreOperationKind::SkipUnavailable {
                        reason: "missing pre-image blob".to_string(),
                    }
                } else {
                    RestoreOperationKind::Restore {
                        pre: pre.clone(),
                        expected_current,
                    }
                }
            }
            (None, Some(expected_current)) => {
                RestoreOperationKind::DeleteCreated { expected_current }
            }
            (Some(pre), None) => {
                if store.blob_path(&pre.sha256).exists() {
                    RestoreOperationKind::ResurrectDeleted { pre: pre.clone() }
                } else {
                    RestoreOperationKind::SkipUnavailable {
                        reason: "missing pre-image blob".to_string(),
                    }
                }
            }
            (None, None) => RestoreOperationKind::SkipUnavailable {
                reason: "missing pre and post images".to_string(),
            },
        };
        operations.push(RestoreOperation {
            relative_path,
            kind,
        });
    }
    operations.sort_by(|left, right| left.relative_path.cmp(&right.relative_path));
    RestorePlan {
        session_id: session_id.to_string(),
        target_turn,
        latest_turn,
        operations,
        diagnostics: read.diagnostics.clone(),
    }
}