From 9cd8d593fb83e4a5e793bd3511cd0b941b714ffb Mon Sep 17 00:00:00 2001 From: zhukang <274546966@qq.com> Date: Mon, 23 Mar 2026 17:40:07 +0800 Subject: [PATCH] ralph-loop[epic-a-to-d-mainline]: iteration 3 --- ...tate-and-Recovery-Payload-Compatibility.md | 31 +++++++++++++++++++ Docs/Architecture.md | 3 +- Docs/DECISIONS.md | 7 +++++ Docs/Protocol.md | 23 ++++++++++++-- Docs/TaskStateMachine.md | 1 + .../Sources/AtlasProtocol/AtlasProtocol.swift | 2 +- 6 files changed, 63 insertions(+), 4 deletions(-) create mode 100644 Docs/ADR/ADR-008-Versioned-Workspace-State-and-Recovery-Payload-Compatibility.md diff --git a/Docs/ADR/ADR-008-Versioned-Workspace-State-and-Recovery-Payload-Compatibility.md b/Docs/ADR/ADR-008-Versioned-Workspace-State-and-Recovery-Payload-Compatibility.md new file mode 100644 index 0000000..a22f1ab --- /dev/null +++ b/Docs/ADR/ADR-008-Versioned-Workspace-State-and-Recovery-Payload-Compatibility.md @@ -0,0 +1,31 @@ +# ADR-008: Versioned Workspace State and Recovery Payload Compatibility + +## Status + +Accepted + +## Context + +Atlas persists a local workspace-state file and stores recovery payloads for both `Finding` and app uninstall flows. Those payloads have already evolved in place: app recovery entries gained uninstall evidence, and the active roadmap now explicitly calls for payload stability, older-state compatibility, and more trustworthy history behavior. + +Without an explicit persistence envelope, Atlas can only rely on best-effort shape decoding. That makes future recovery hardening riskier and leaves migration implicit. The `Apps` flow also risks showing stale preview or stale footprint counts after restore unless restored app payloads are reconciled with fresh app inventory. + +## Decision + +- Atlas persists workspace state inside a versioned JSON envelope containing `schemaVersion`, `savedAt`, `snapshot`, `currentPlan`, and `settings`. +- Atlas continues to decode legacy top-level `AtlasWorkspaceState` files and rewrites them into the current envelope after a successful load when possible. +- `AppRecoveryPayload` carries an explicit `schemaVersion` and remains backward-compatible with the older raw-`AppFootprint` recovery payload shape. +- App restore flows clear stale uninstall preview state and refresh app inventory before the `Apps` surface reuses footprint counts. + +## Consequences + +- Atlas now has an explicit persistence contract for future migration work instead of relying on implicit shape matching alone. +- Older state files remain loadable while the repo transitions to the versioned envelope. +- App recovery payloads become safer to evolve because compatibility is now a stated requirement. +- The `Apps` surface becomes more trustworthy after restore because it no longer depends only on stale pre-uninstall preview state. + +## Alternatives Considered + +- Keep the unversioned top-level state file and rely on ad hoc per-type decoding: rejected because it scales poorly as recovery payloads evolve. +- Break compatibility and require a fresh state file: rejected because it damages trust in `History` and `Recovery`. +- Refresh app inventory only on explicit user action after restore: rejected because it leaves a visible stale-evidence gap in a trust-critical workflow. diff --git a/Docs/Architecture.md b/Docs/Architecture.md index 0766a82..b5bf8aa 100644 --- a/Docs/Architecture.md +++ b/Docs/Architecture.md @@ -39,7 +39,7 @@ ### Infrastructure - XPC transport -- JSON-backed workspace state persistence +- Versioned JSON-backed workspace state persistence with legacy-shape migration on load - Recovery-state normalization that prunes expired recovery entries on load/save - Logging and audit events - Best-effort permission inspection @@ -56,6 +56,7 @@ - Release-facing execution must fail closed when real worker/adapter/helper capability is unavailable; scaffold fallback is development-only by opt-in - Smart Clean now supports a real Trash-based execution path for a safe structured subset of user-owned targets, plus physical restoration when recovery mappings are present - Restore requests recheck expiry and destination conflicts before side effects, so expired or conflicting recovery items fail closed +- App recovery payloads now carry an explicit schema version, and app restores should be followed by inventory refresh so the `Apps` surface does not keep stale footprint evidence ## Process Boundaries diff --git a/Docs/DECISIONS.md b/Docs/DECISIONS.md index 97575a3..eba72de 100644 --- a/Docs/DECISIONS.md +++ b/Docs/DECISIONS.md @@ -67,6 +67,13 @@ - `Storage treemap`, `Menu Bar`, and `Automation` remain out of scope unless the decision log is updated explicitly - Atlas should compete as an `explainable, recovery-first Mac maintenance workspace`, not as a generic all-in-one cleaner +### D-011 Versioned Workspace State and Recovery Payload Compatibility + +- Persisted workspace state uses a versioned JSON envelope instead of an unversioned top-level payload +- Atlas must continue decoding older top-level workspace-state files and rewrite them into the current envelope when possible +- App recovery payloads carry an explicit schema version and must remain backward-compatible with legacy app-only recovery payload shapes +- App payload restores must refresh app inventory before `Apps` reuses footprint counts or uninstall preview state + ## Update Rule Add a new decision entry whenever product scope, protocol, privilege boundaries, release route, or recovery model changes. diff --git a/Docs/Protocol.md b/Docs/Protocol.md index 86c0548..f146ade 100644 --- a/Docs/Protocol.md +++ b/Docs/Protocol.md @@ -8,7 +8,7 @@ ## Protocol Version -- Current implementation version: `0.3.1` +- Current implementation version: `0.3.2` ## UI ↔ Worker Commands @@ -127,9 +127,26 @@ ### AppRecoveryPayload +- `schemaVersion` - `app` - `uninstallEvidence` +## Workspace State Persistence + +Atlas persists local workspace state in a versioned JSON envelope: + +- `schemaVersion` +- `savedAt` +- `snapshot` +- `currentPlan` +- `settings` + +Compatibility rules: + +- legacy top-level `AtlasWorkspaceState` files must still decode on load +- after a successful legacy decode, Atlas may rewrite the file into the current versioned envelope +- legacy app recovery payloads that stored a raw `AppFootprint` must still decode into the current `AppRecoveryPayload` shape + ### AtlasSettings - `recoveryRetentionDays` @@ -154,7 +171,8 @@ - `health.snapshot` is backed by `lib/check/health_json.sh` through `MoleHealthAdapter`. - `scan.start` is backed by `bin/clean.sh --dry-run` through `MoleSmartCleanAdapter` when the upstream workflow succeeds. If it cannot complete, the worker now rejects the request instead of silently fabricating scan results. - `apps.list` is backed by `MacAppsInventoryAdapter`, which scans local app bundles and derives lightweight leftover counts suitable for interactive refresh. -- The worker persists a local JSON-backed workspace state containing the latest snapshot, current Smart Clean plan, and settings, including the persisted app-language preference. +- The worker persists a versioned local JSON workspace state containing the latest snapshot, current Smart Clean plan, and settings, including the persisted app-language preference. +- Legacy top-level workspace-state files are migrated on load into the current versioned envelope when possible. - The repository and worker normalize recovery state by pruning expired `RecoveryItem`s and rejecting restore requests that arrive after the retention window has closed. - Atlas localizes user-facing shell copy through a package-scoped resource bundle and uses the persisted language to keep summaries and settings text aligned. - App uninstall can invoke the packaged or development helper executable through structured JSON actions. @@ -166,3 +184,4 @@ - `executePlan` is fail-closed for unsupported targets, but now supports a real Trash-based execution path for a safe structured subset of Smart Clean items. - `recovery.restore` can physically restore items when `restoreMappings` are present; otherwise it falls back to model rehydration only. - `recovery.restore` rejects expired recovery items with `restoreExpired` and rejects destination collisions with `restoreConflict`. +- App payload restores should be followed by app-inventory refresh so the `Apps` surface does not reuse stale uninstall preview or stale footprint counts after recovery. diff --git a/Docs/TaskStateMachine.md b/Docs/TaskStateMachine.md index f85ef7d..7d7fa6b 100644 --- a/Docs/TaskStateMachine.md +++ b/Docs/TaskStateMachine.md @@ -67,4 +67,5 @@ - `execute_uninstall` removes an app from the current workspace view and creates a recovery entry. - `restore` can physically restore items when structured recovery mappings are present, and can still rehydrate a `Finding` or an app payload into Atlas state from the recovery payload. - `restore` must reject expired recovery items before side effects and must fail closed when the original destination already exists. +- When `restore` rehydrates an app payload, the `Apps` surface should refresh inventory before presenting footprint counts or a new uninstall preview. - User-visible task summaries and settings-driven text should reflect the persisted app-language preference when generated. diff --git a/Packages/AtlasProtocol/Sources/AtlasProtocol/AtlasProtocol.swift b/Packages/AtlasProtocol/Sources/AtlasProtocol/AtlasProtocol.swift index a7fe231..b5e1021 100644 --- a/Packages/AtlasProtocol/Sources/AtlasProtocol/AtlasProtocol.swift +++ b/Packages/AtlasProtocol/Sources/AtlasProtocol/AtlasProtocol.swift @@ -2,7 +2,7 @@ import AtlasDomain import Foundation public enum AtlasProtocolVersion { - public static let current = "0.3.1" + public static let current = "0.3.2" } public struct AtlasCapabilityStatus: Codable, Hashable, Sendable {