diff --git a/Apps/AtlasApp/Sources/AtlasApp/AppShellView.swift b/Apps/AtlasApp/Sources/AtlasApp/AppShellView.swift index 66853f9..4def122 100644 --- a/Apps/AtlasApp/Sources/AtlasApp/AppShellView.swift +++ b/Apps/AtlasApp/Sources/AtlasApp/AppShellView.swift @@ -145,6 +145,7 @@ struct AppShellView: View { isCurrentPlanFresh: model.isCurrentSmartCleanPlanFresh, canExecutePlan: model.canExecuteCurrentSmartCleanPlan, planIssue: model.smartCleanPlanIssue, + executionIssue: model.smartCleanExecutionIssue, onStartScan: { Task { await model.runSmartCleanScan() } }, diff --git a/Apps/AtlasApp/Sources/AtlasApp/AtlasAppModel.swift b/Apps/AtlasApp/Sources/AtlasApp/AtlasAppModel.swift index e72ac53..816b552 100644 --- a/Apps/AtlasApp/Sources/AtlasApp/AtlasAppModel.swift +++ b/Apps/AtlasApp/Sources/AtlasApp/AtlasAppModel.swift @@ -31,6 +31,7 @@ final class AtlasAppModel: ObservableObject { @Published private(set) var latestScanProgress: Double = 0 @Published private(set) var isCurrentSmartCleanPlanFresh: Bool @Published private(set) var smartCleanPlanIssue: String? + @Published private(set) var smartCleanExecutionIssue: String? @Published private(set) var latestUpdateResult: AtlasAppUpdate? @Published private(set) var isCheckingForUpdate = false @Published private(set) var updateCheckNotice: String? @@ -45,6 +46,10 @@ final class AtlasAppModel: ObservableObject { init( repository: AtlasWorkspaceRepository = AtlasWorkspaceRepository(), workerService: (any AtlasWorkerServing)? = nil, + preferXPCWorker: Bool? = nil, + allowScaffoldFallback: Bool? = nil, + xpcRequestConfiguration: AtlasXPCRequestConfiguration = AtlasXPCRequestConfiguration(), + xpcRequestExecutor: AtlasXPCDataRequestExecutor? = nil, notificationPermissionRequester: (@Sendable () async -> Bool)? = nil ) { let state = repository.loadState() @@ -57,6 +62,7 @@ final class AtlasAppModel: ObservableObject { self.latestPermissionsSummary = AtlasL10n.string("model.permissions.ready") self.isCurrentSmartCleanPlanFresh = false self.smartCleanPlanIssue = nil + self.smartCleanExecutionIssue = nil let directWorker = AtlasScaffoldWorkerService( repository: repository, healthSnapshotProvider: MoleHealthAdapter(), @@ -64,11 +70,15 @@ final class AtlasAppModel: ObservableObject { appsInventoryProvider: MacAppsInventoryAdapter(), helperExecutor: AtlasPrivilegedHelperClient() ) - let prefersXPCWorker = ProcessInfo.processInfo.environment["ATLAS_PREFER_XPC_WORKER"] == "1" + let prefersXPCWorker = preferXPCWorker ?? (ProcessInfo.processInfo.environment["ATLAS_PREFER_XPC_WORKER"] == "1") + let shouldAllowScaffoldFallback = allowScaffoldFallback + ?? (ProcessInfo.processInfo.environment["ATLAS_ALLOW_SCAFFOLD_FALLBACK"] == "1") let defaultWorker: any AtlasWorkerServing = prefersXPCWorker ? AtlasPreferredWorkerService( + requestConfiguration: xpcRequestConfiguration, + requestExecutor: xpcRequestExecutor, fallbackWorker: directWorker, - allowFallback: true + allowFallback: shouldAllowScaffoldFallback ) : directWorker self.workspaceController = AtlasWorkspaceController( @@ -305,6 +315,7 @@ final class AtlasAppModel: ObservableObject { isScanRunning = true latestScanSummary = AtlasL10n.string("model.scan.submitting") latestScanProgress = 0 + smartCleanExecutionIssue = nil do { let output = try await workspaceController.startScan() @@ -315,6 +326,7 @@ final class AtlasAppModel: ObservableObject { latestScanProgress = output.progressFraction isCurrentSmartCleanPlanFresh = output.actionPlan != nil smartCleanPlanIssue = nil + smartCleanExecutionIssue = nil } } catch { latestScanSummary = error.localizedDescription @@ -327,6 +339,7 @@ final class AtlasAppModel: ObservableObject { @discardableResult func refreshPlanPreview() async -> Bool { + smartCleanExecutionIssue = nil do { let output = try await workspaceController.previewPlan(findingIDs: snapshot.findings.map(\.id)) withAnimation(.snappy(duration: 0.24)) { @@ -336,6 +349,7 @@ final class AtlasAppModel: ObservableObject { latestScanProgress = min(max(latestScanProgress, 1), 1) isCurrentSmartCleanPlanFresh = true smartCleanPlanIssue = nil + smartCleanExecutionIssue = nil } return true } catch { @@ -352,6 +366,7 @@ final class AtlasAppModel: ObservableObject { selection = .smartClean isPlanRunning = true + smartCleanExecutionIssue = nil do { let output = try await workspaceController.executePlan(planID: currentPlan.id) @@ -360,6 +375,7 @@ final class AtlasAppModel: ObservableObject { latestScanSummary = output.summary latestScanProgress = output.progressFraction smartCleanPlanIssue = nil + smartCleanExecutionIssue = nil } let didRefreshPlan = await refreshPlanPreview() if !didRefreshPlan { @@ -367,7 +383,7 @@ final class AtlasAppModel: ObservableObject { } } catch { latestScanSummary = error.localizedDescription - smartCleanPlanIssue = error.localizedDescription + smartCleanExecutionIssue = error.localizedDescription } isPlanRunning = false @@ -463,6 +479,7 @@ final class AtlasAppModel: ObservableObject { withAnimation(.snappy(duration: 0.24)) { snapshot = output.snapshot latestScanSummary = output.summary + smartCleanExecutionIssue = nil } await refreshPlanPreview() } catch { diff --git a/Apps/AtlasApp/Tests/AtlasAppTests/AtlasAppModelTests.swift b/Apps/AtlasApp/Tests/AtlasAppTests/AtlasAppModelTests.swift index c470da5..16501bf 100644 --- a/Apps/AtlasApp/Tests/AtlasAppTests/AtlasAppModelTests.swift +++ b/Apps/AtlasApp/Tests/AtlasAppTests/AtlasAppModelTests.swift @@ -3,6 +3,7 @@ import XCTest import AtlasApplication import AtlasDomain import AtlasInfrastructure +import AtlasProtocol @MainActor final class AtlasAppModelTests: XCTestCase { @@ -92,6 +93,49 @@ final class AtlasAppModelTests: XCTestCase { XCTAssertGreaterThan(model.latestScanProgress, 0) } + func testExecuteCurrentPlanExposesExplicitExecutionIssueWhenWorkerRejectsExecution() async { + let repository = makeRepository() + let model = AtlasAppModel( + repository: repository, + workerService: RejectingWorker(code: .executionUnavailable, reason: "XPC worker offline") + ) + + await model.executeCurrentPlan() + + XCTAssertFalse(model.isPlanRunning) + XCTAssertEqual(model.smartCleanExecutionIssue, AtlasL10n.string("application.error.executionUnavailable", "XPC worker offline")) + XCTAssertEqual(model.latestScanSummary, AtlasL10n.string("application.error.executionUnavailable", "XPC worker offline")) + } + + func testPreferredXPCWorkerPathFailsClosedWhenScanIsRejected() async throws { + let repository = makeRepository() + let rejectedRequest = AtlasRequestEnvelope(command: .startScan(taskID: UUID())) + let rejectedResult = AtlasWorkerCommandResult( + request: rejectedRequest, + response: AtlasResponseEnvelope( + requestID: rejectedRequest.id, + response: .rejected(code: .executionUnavailable, reason: "simulated packaged worker failure") + ), + events: [], + snapshot: AtlasScaffoldWorkspace.snapshot(language: .en), + previewPlan: nil + ) + let responseData = try JSONEncoder().encode(rejectedResult) + let model = AtlasAppModel( + repository: repository, + preferXPCWorker: true, + allowScaffoldFallback: false, + xpcRequestConfiguration: AtlasXPCRequestConfiguration(timeout: 1, retryCount: 0, retryDelay: 0), + xpcRequestExecutor: { _ in responseData } + ) + + await model.runSmartCleanScan() + + XCTAssertFalse(model.isCurrentSmartCleanPlanFresh) + XCTAssertEqual(model.smartCleanPlanIssue, AtlasL10n.string("application.error.executionUnavailable", "simulated packaged worker failure")) + XCTAssertFalse(model.latestScanSummary.contains("reclaimable item")) + } + func testRefreshAppsUsesInventoryProvider() async throws { let repository = makeRepository() let worker = AtlasScaffoldWorkerService( @@ -122,6 +166,29 @@ final class AtlasAppModelTests: XCTestCase { XCTAssertFalse(model.snapshot.recoveryItems.contains(where: { $0.id == recoveryItemID })) } + func testRestoreRecoveryItemClearsPreviousSmartCleanExecutionIssue() async throws { + let repository = makeRepository() + let realWorker = AtlasScaffoldWorkerService(repository: repository, allowStateOnlyCleanExecution: true) + let seededState = repository.loadState() + XCTAssertFalse(seededState.snapshot.recoveryItems.isEmpty) + let recoveryItemID = try XCTUnwrap(seededState.snapshot.recoveryItems.first?.id) + let model = AtlasAppModel( + repository: repository, + workerService: ExecuteRejectingRestoreDelegatingWorker( + code: .executionUnavailable, + reason: "XPC worker offline", + restoreWorker: realWorker + ) + ) + + await model.executeCurrentPlan() + XCTAssertNotNil(model.smartCleanExecutionIssue) + + await model.restoreRecoveryItem(recoveryItemID) + + XCTAssertNil(model.smartCleanExecutionIssue) + } + func testSettingsUpdatePersistsThroughWorker() async throws { let repository = makeRepository() let permissionInspector = AtlasPermissionInspector( @@ -295,3 +362,56 @@ private actor NotificationPermissionRecorder { calls } } + +private actor RejectingWorker: AtlasWorkerServing { + let code: AtlasProtocolErrorCode + let reason: String + + init(code: AtlasProtocolErrorCode, reason: String) { + self.code = code + self.reason = reason + } + + func submit(_ request: AtlasRequestEnvelope) async throws -> AtlasWorkerCommandResult { + AtlasWorkerCommandResult( + request: request, + response: AtlasResponseEnvelope( + requestID: request.id, + response: .rejected(code: code, reason: reason) + ), + events: [], + snapshot: AtlasScaffoldWorkspace.snapshot(language: .en), + previewPlan: nil + ) + } +} + +private actor ExecuteRejectingRestoreDelegatingWorker: AtlasWorkerServing { + let code: AtlasProtocolErrorCode + let reason: String + let restoreWorker: AtlasScaffoldWorkerService + + init(code: AtlasProtocolErrorCode, reason: String, restoreWorker: AtlasScaffoldWorkerService) { + self.code = code + self.reason = reason + self.restoreWorker = restoreWorker + } + + func submit(_ request: AtlasRequestEnvelope) async throws -> AtlasWorkerCommandResult { + switch request.command { + case .executePlan: + return AtlasWorkerCommandResult( + request: request, + response: AtlasResponseEnvelope( + requestID: request.id, + response: .rejected(code: code, reason: reason) + ), + events: [], + snapshot: AtlasScaffoldWorkspace.snapshot(language: .en), + previewPlan: nil + ) + default: + return try await restoreWorker.submit(request) + } + } +} diff --git a/Docs/Backlog.md b/Docs/Backlog.md index 0fedf0e..87a5b70 100644 --- a/Docs/Backlog.md +++ b/Docs/Backlog.md @@ -46,6 +46,11 @@ - `EPIC-08` Permissions and System Integration - `EPIC-09` Quality and Verification - `EPIC-10` Packaging, Signing, and Release +- `EPIC-16` Beta Stabilization and Execution Truthfulness +- `EPIC-17` Signed Public Beta Packaging +- `EPIC-18` Public Beta Feedback and Trust Closure +- `EPIC-19` GA Recovery and Execution Hardening +- `EPIC-20` GA Launch Readiness ## Now / Next / Later @@ -195,6 +200,88 @@ - `ATL-115` Measure perceived latency and remove avoidable visual jumps in core flows — `QA Agent` - `ATL-116` Polish Week 2 gate review — `Product Agent` +## Internal Beta Hardening Track + +### Current Status + +- `Complete` — frozen MVP is implemented and internally beta-ready. +- `Blocked` — release trust still depends on removing silent fallback and tightening execution/recovery honesty. +- `Dormant` — signed public beta work is inactive until Apple signing/notarization credentials exist. + +### Focus + +- Keep the roadmap inside the frozen MVP modules. +- Hard-fix execution truthfulness before any broader distribution plan resumes. +- Make recovery claims match shipped restore behavior. +- Keep signed public beta work as a conditional branch, not the active mainline. + +### Epics + +- `EPIC-16` Beta Stabilization and Execution Truthfulness +- `EPIC-17` Signed Public Beta Packaging +- `EPIC-18` Public Beta Feedback and Trust Closure +- `EPIC-19` GA Recovery and Execution Hardening +- `EPIC-20` GA Launch Readiness + +### Now / Next / Later + +#### Now + +- Remove or gate silent fallback in release-facing execution flows +- Run bilingual manual QA on a clean machine +- Validate packaged first-launch behavior with a fresh state file +- Tighten release-facing copy where execution or recovery is overstated + +#### Next + +- Expand real `Smart Clean` execute coverage for the highest-value safe targets +- Add stronger `scan -> execute -> rescan` contract coverage +- Implement physical restore for file-backed recoverable actions, or narrow product claims +- Freeze recovery-related copy only after behavior is proven + +#### Later + +- Obtain Apple signing and notarization credentials +- Produce signed and notarized `.app`, `.dmg`, and `.pkg` artifacts +- Validate signed install behavior on a clean machine +- Run a small hardware-diverse public beta cohort only after signed distribution is available + +### Seed Issues + +#### Release Phase 1: Beta Stabilization + +- `ATL-201` Remove or development-gate silent XPC fallback in release-facing execution flows — `System Agent` +- `ATL-202` Add explicit failure states when real worker execution is unavailable — `Mac App Agent` +- `ATL-203` Run bilingual manual QA on a clean machine — `QA Agent` +- `ATL-204` Validate fresh-state first launch from packaged artifacts — `QA Agent` +- `ATL-205` Narrow release-facing recovery and execution copy where needed — `UX Agent` + `Docs Agent` +- `ATL-206` Beta stabilization gate review — `Product Agent` + +#### Release Phase 2: Smart Clean Execution Credibility + +- `ATL-211` Expand real `Smart Clean` execute coverage for top safe target classes — `System Agent` +- `ATL-212` Carry executable structured targets through the worker path — `Core Agent` +- `ATL-213` Add stronger `scan -> execute -> rescan` contract coverage — `QA Agent` +- `ATL-214` Make history and completion states reflect real side effects only — `Mac App Agent` +- `ATL-215` Execution credibility gate review — `Product Agent` + +#### Release Phase 3: Recovery Credibility + +- `ATL-221` Implement physical restore for file-backed recoverable actions where safe — `System Agent` +- `ATL-222` Validate shipped restore behavior on real file-backed test cases — `QA Agent` +- `ATL-223` Narrow README, in-app, and release-note recovery claims if needed — `Docs Agent` + `Product Agent` +- `ATL-224` Freeze recovery contract and acceptance evidence — `Product Agent` +- `ATL-225` Recovery credibility gate review — `Product Agent` + +#### Conditional Release Phase 4: Signed Distribution and External Beta + +- `ATL-231` Obtain Apple release signing credentials — `Release Agent` +- `ATL-232` Pass `signing-preflight.sh` on the release machine — `Release Agent` +- `ATL-233` Produce signed and notarized native artifacts — `Release Agent` +- `ATL-234` Validate signed DMG and PKG install on a clean machine — `QA Agent` +- `ATL-235` Run a trusted hardware-diverse signed beta cohort — `Product Agent` +- `ATL-236` Triage public-beta issues before any GA candidate naming — `Product Agent` + ## Definition of Ready - Scope is clear and bounded diff --git a/Docs/Execution/Beta-Gate-Review.md b/Docs/Execution/Beta-Gate-Review.md index b07c9cc..50de2be 100644 --- a/Docs/Execution/Beta-Gate-Review.md +++ b/Docs/Execution/Beta-Gate-Review.md @@ -54,6 +54,7 @@ - Core frozen MVP workflows are complete end to end. - Recovery-first behavior is visible in both Smart Clean and Apps flows. +- Physical on-disk restore is currently limited to recovery items that carry a supported restore path; older or unstructured records may still be model-only restore. - Settings and permission refresh flows are functional. - The app now defaults to `简体中文` and supports switching to `English` through persisted settings. @@ -85,6 +86,7 @@ ## Conditions - Internal beta / trusted-user beta can proceed with the current ad hoc-signed local artifacts. +- Recovery and execution copy must stay explicit about supported restore paths and unsupported cleanup targets during internal beta. - Public beta or broad external distribution must wait until signing and notarization credentials are available and the release packaging path is re-run. ## Follow-up Actions diff --git a/Docs/Execution/Current-Status-2026-03-07.md b/Docs/Execution/Current-Status-2026-03-07.md index 7217742..9adce37 100644 --- a/Docs/Execution/Current-Status-2026-03-07.md +++ b/Docs/Execution/Current-Status-2026-03-07.md @@ -54,7 +54,7 @@ ## Current Blockers -- `Smart Clean` execute now supports a real Trash-based path for structured safe targets, and those targets can be physically restored. Full disk-backed coverage is still incomplete, and unsupported targets fail closed. See `Docs/Execution/Execution-Chain-Audit-2026-03-09.md`. +- `Smart Clean` execute now supports a real Trash-based path for structured safe targets, and those targets can be physically restored when recovery mappings are present. Full disk-backed coverage is still incomplete, and unsupported targets fail closed. See `Docs/Execution/Execution-Chain-Audit-2026-03-09.md`. - Silent fallback from XPC to the scaffold worker can mask execution-path failures in user-facing flows. See `Docs/Execution/Execution-Chain-Audit-2026-03-09.md`. - Public signed distribution is still blocked by missing Apple release credentials: - `Developer ID Application` diff --git a/Docs/Execution/Implementation-Plan-ATL-201-202-205-2026-03-12.md b/Docs/Execution/Implementation-Plan-ATL-201-202-205-2026-03-12.md new file mode 100644 index 0000000..14f1a26 --- /dev/null +++ b/Docs/Execution/Implementation-Plan-ATL-201-202-205-2026-03-12.md @@ -0,0 +1,370 @@ +# ATL-201 / ATL-202 / ATL-205 Implementation Plan + +> **For Codex:** Execute this plan task-by-task. Keep changes small, verify each layer narrowly first, and commit frequently. + +**Goal:** Remove release-facing silent scaffold fallback, expose explicit execution-unavailable failures in the app UI, and narrow bilingual execution/recovery copy so Atlas only claims behavior it can actually prove today. + +**Architecture:** Keep the worker fallback capability available only as an explicit development override, but make the default app path fail closed. Surface worker rejection reasons through the application layer as user-facing localized errors, then render those errors in `Smart Clean` as a danger-state callout instead of a quiet summary string. Tighten copy in the app and release-facing docs so “recoverable” and “restore” only describe the currently shipped behavior. + +**Tech Stack:** Swift 6, SwiftUI, Swift Package Manager tests, package-scoped localization resources, Markdown docs. + +--- + +### Task 1: Lock Release-Facing Worker Fallback Behind Explicit Development Mode + +**Files:** +- Modify: `Apps/AtlasApp/Sources/AtlasApp/AtlasAppModel.swift` +- Modify: `Packages/AtlasInfrastructure/Sources/AtlasInfrastructure/AtlasXPCTransport.swift` +- Test: `Packages/AtlasInfrastructure/Tests/AtlasInfrastructureTests/AtlasXPCTransportTests.swift` +- Test: `Apps/AtlasApp/Tests/AtlasAppTests/AtlasAppModelTests.swift` + +**Step 1: Write the failing transport tests** + +Add tests in `Packages/AtlasInfrastructure/Tests/AtlasInfrastructureTests/AtlasXPCTransportTests.swift` that cover: + +- release/default mode does **not** fall back when XPC rejects `executionUnavailable` +- explicit development mode still can fall back when `allowFallback == true` + +Use the existing rejected-result fixture style: + +```swift +let rejected = AtlasWorkerCommandResult( + request: request, + response: AtlasResponseEnvelope( + requestID: request.id, + response: .rejected(code: .executionUnavailable, reason: "simulated packaged worker failure") + ), + events: [], + snapshot: AtlasScaffoldWorkspace.snapshot(), + previewPlan: nil +) +``` + +**Step 2: Run the transport tests to verify the current fallback behavior** + +Run: + +```bash +swift test --package-path Packages --filter AtlasXPCTransportTests +``` + +Expected: + +- one new test fails because the current app-facing setup still allows fallback when XPC rejects `executionUnavailable` + +**Step 3: Make fallback opt-in for development only** + +In `Packages/AtlasInfrastructure/Sources/AtlasInfrastructure/AtlasXPCTransport.swift` and `Apps/AtlasApp/Sources/AtlasApp/AtlasAppModel.swift`: + +- keep `AtlasPreferredWorkerService` capable of fallback for explicit dev usage +- stop hardcoding `allowFallback: true` in `AtlasAppModel` +- derive fallback permission from an explicit development-only env path, for example: + +```swift +let allowScaffoldFallback = ProcessInfo.processInfo.environment["ATLAS_ALLOW_SCAFFOLD_FALLBACK"] == "1" +``` + +- pass that value into `AtlasPreferredWorkerService(...)` + +The key result is: + +- installed/release-facing app path fails closed by default +- developers can still opt in locally with an env var + +**Step 4: Add an app-model test for the default worker policy** + +In `Apps/AtlasApp/Tests/AtlasAppTests/AtlasAppModelTests.swift`, add a focused test around a worker rejection path using an injected fake worker or a small helper constructor so the app model no longer assumes fallback-enabled behavior in default/release configuration. + +Target behavior: + +```swift +XCTAssertFalse(model.canExecuteCurrentSmartCleanPlan) +XCTAssertTrue(model.latestScanSummary.contains("unavailable")) +``` + +**Step 5: Run tests and verify** + +Run: + +```bash +swift test --package-path Packages --filter AtlasXPCTransportTests +swift test --package-path Apps --filter AtlasAppModelTests +``` + +Expected: + +- transport tests pass +- app-model tests still pass with the new fail-closed default + +**Step 6: Commit** + +```bash +git add Packages/AtlasInfrastructure/Sources/AtlasInfrastructure/AtlasXPCTransport.swift Apps/AtlasApp/Sources/AtlasApp/AtlasAppModel.swift Packages/AtlasInfrastructure/Tests/AtlasInfrastructureTests/AtlasXPCTransportTests.swift Apps/AtlasApp/Tests/AtlasAppTests/AtlasAppModelTests.swift +git commit -m "fix: gate scaffold fallback behind dev mode" +``` + +### Task 2: Surface Explicit Execution-Unavailable Failure States in Smart Clean + +**Files:** +- Modify: `Packages/AtlasApplication/Sources/AtlasApplication/AtlasApplication.swift` +- Modify: `Packages/AtlasDomain/Sources/AtlasDomain/Resources/en.lproj/Localizable.strings` +- Modify: `Packages/AtlasDomain/Sources/AtlasDomain/Resources/zh-Hans.lproj/Localizable.strings` +- Modify: `Apps/AtlasApp/Sources/AtlasApp/AtlasAppModel.swift` +- Modify: `Apps/AtlasApp/Sources/AtlasApp/AppShellView.swift` +- Modify: `Packages/AtlasFeaturesSmartClean/Sources/AtlasFeaturesSmartClean/SmartCleanFeatureView.swift` +- Test: `Packages/AtlasApplication/Tests/AtlasApplicationTests/AtlasApplicationTests.swift` +- Test: `Apps/AtlasApp/Tests/AtlasAppTests/AtlasAppModelTests.swift` + +**Step 1: Write the failing application-layer tests** + +Add tests in `Packages/AtlasApplication/Tests/AtlasApplicationTests/AtlasApplicationTests.swift` for: + +- `executionUnavailable` rejection maps to a user-facing localized error +- `helperUnavailable` rejection maps to a user-facing localized error + +Use the existing `FakeWorker` and a rejected response: + +```swift +let result = AtlasWorkerCommandResult( + request: request, + response: AtlasResponseEnvelope( + requestID: request.id, + response: .rejected(code: .executionUnavailable, reason: "XPC worker offline") + ), + events: [], + snapshot: AtlasScaffoldWorkspace.snapshot(), + previewPlan: nil +) +``` + +Assert on `error.localizedDescription`. + +**Step 2: Run the application tests to verify they fail** + +Run: + +```bash +swift test --package-path Packages --filter AtlasApplicationTests +``` + +Expected: + +- new rejection-mapping tests fail because `AtlasWorkspaceControllerError` still uses the generic `application.error.workerRejected` + +**Step 3: Add code-specific user-facing error strings** + +In `Packages/AtlasApplication/Sources/AtlasApplication/AtlasApplication.swift`, replace the one-size-fits-all rejection mapping with explicit cases: + +```swift +case let .rejected(code, reason): + switch code { + case .executionUnavailable: + return AtlasL10n.string("application.error.executionUnavailable", reason) + case .helperUnavailable: + return AtlasL10n.string("application.error.helperUnavailable", reason) + default: + return AtlasL10n.string("application.error.workerRejected", code.rawValue, reason) + } +``` + +Add matching bilingual keys in both `.strings` files. + +**Step 4: Write the failing app-model/UI-state tests** + +In `Apps/AtlasApp/Tests/AtlasAppTests/AtlasAppModelTests.swift`, add tests that verify: + +- executing a plan with `executionUnavailable` leaves the plan non-busy +- the model stores an explicit Smart Clean execution issue +- the summary and/or issue text is specific, not a silent generic fallback success + +Introduce a small fake worker actor in the test file if needed: + +```swift +private actor RejectingWorker: AtlasWorkerServing { + func submit(_ request: AtlasRequestEnvelope) async throws -> AtlasWorkerCommandResult { ... } +} +``` + +**Step 5: Implement explicit Smart Clean failure state** + +In `Apps/AtlasApp/Sources/AtlasApp/AtlasAppModel.swift`: + +- add a dedicated published property such as: + +```swift +@Published private(set) var smartCleanExecutionIssue: String? +``` + +- clear it before a new scan / preview / execute attempt +- set it when `executeCurrentPlan()` catches `executionUnavailable` or `helperUnavailable` + +In `Apps/AtlasApp/Sources/AtlasApp/AppShellView.swift`, pass the new issue through to `SmartCleanFeatureView`. + +In `Packages/AtlasFeaturesSmartClean/Sources/AtlasFeaturesSmartClean/SmartCleanFeatureView.swift`: + +- add an optional `executionIssue: String?` +- render a danger-tone callout or status state when this value exists +- prefer this explicit issue over a normal “ready” or cached-plan state + +The UI target is: + +- a failed real execution attempt is visually obvious +- the user sees “unavailable” or “helper unavailable”, not just a stale summary string + +**Step 6: Run tests and verify** + +Run: + +```bash +swift test --package-path Packages --filter AtlasApplicationTests +swift test --package-path Apps --filter AtlasAppModelTests +``` + +Expected: + +- rejection mapping tests pass +- app-model tests show explicit failure-state behavior + +**Step 7: Commit** + +```bash +git add Packages/AtlasApplication/Sources/AtlasApplication/AtlasApplication.swift Packages/AtlasDomain/Sources/AtlasDomain/Resources/en.lproj/Localizable.strings Packages/AtlasDomain/Sources/AtlasDomain/Resources/zh-Hans.lproj/Localizable.strings Apps/AtlasApp/Sources/AtlasApp/AtlasAppModel.swift Apps/AtlasApp/Sources/AtlasApp/AppShellView.swift Packages/AtlasFeaturesSmartClean/Sources/AtlasFeaturesSmartClean/SmartCleanFeatureView.swift Packages/AtlasApplication/Tests/AtlasApplicationTests/AtlasApplicationTests.swift Apps/AtlasApp/Tests/AtlasAppTests/AtlasAppModelTests.swift +git commit -m "fix: expose explicit smart clean execution failures" +``` + +### Task 3: Narrow Bilingual Execution and Recovery Copy to Shipped Behavior + +**Files:** +- Modify: `Packages/AtlasDomain/Sources/AtlasDomain/Resources/en.lproj/Localizable.strings` +- Modify: `Packages/AtlasDomain/Sources/AtlasDomain/Resources/zh-Hans.lproj/Localizable.strings` +- Modify: `README.md` +- Modify: `README.zh-CN.md` +- Modify: `Docs/HELP_CENTER_OUTLINE.md` +- Modify: `Docs/Execution/Current-Status-2026-03-07.md` +- Modify: `Docs/Execution/Beta-Gate-Review.md` +- Modify: `Docs/Execution/Smart-Clean-Execution-Coverage-2026-03-09.md` + +**Step 1: Write the copy audit checklist directly into the diff** + +Before editing text, create a short checklist in your working notes and verify every claim against current behavior: + +- does this line claim physical restore for all recoverable items? +- does this line imply History/Recovery always means on-disk restore? +- does this line distinguish “supported restore path” from “model-only restore”? +- does this line imply direct execution for unsupported Smart Clean items? + +**Step 2: Tighten in-app copy first** + +Update the localized strings most likely to overclaim current behavior: + +- `smartclean.execution.coverage.full.detail` +- `smartclean.preview.callout.safe.detail` +- `application.recovery.completed` +- `infrastructure.restore.summary.one` +- `infrastructure.restore.summary.other` +- `history.callout.recovery.detail` +- `history.detail.recovery.callout.available.detail` +- `history.restore.hint` + +The wording rule is: + +- say “when supported” or “when a restore path is available” where true +- avoid implying every recoverable item restores physically +- avoid implying every Smart Clean item is directly executable + +**Step 3: Tighten release-facing docs** + +Update: + +- `README.md` +- `README.zh-CN.md` +- `Docs/HELP_CENTER_OUTLINE.md` +- `Docs/Execution/Current-Status-2026-03-07.md` +- `Docs/Execution/Beta-Gate-Review.md` +- `Docs/Execution/Smart-Clean-Execution-Coverage-2026-03-09.md` + +Specific goals: + +- README should keep “recovery-first” as a principle without implying universal physical restore +- `Current-Status` and `Beta-Gate-Review` must match the current subset reality +- help content should explicitly include the case where restore cannot return a file physically + +**Step 4: Run the narrowest verification** + +Run: + +```bash +swift test --package-path Packages --filter AtlasApplicationTests +swift test --package-path Apps --filter AtlasAppModelTests +``` + +Then manually verify: + +- Smart Clean empty/ready/error copy still reads correctly in both Chinese and English +- History / Recovery copy still makes sense after narrowing restore claims + +Expected: + +- tests stay green +- docs and UI no longer overclaim physical restore or execution breadth + +**Step 5: Commit** + +```bash +git add Packages/AtlasDomain/Sources/AtlasDomain/Resources/en.lproj/Localizable.strings Packages/AtlasDomain/Sources/AtlasDomain/Resources/zh-Hans.lproj/Localizable.strings README.md README.zh-CN.md Docs/HELP_CENTER_OUTLINE.md Docs/Execution/Current-Status-2026-03-07.md Docs/Execution/Beta-Gate-Review.md Docs/Execution/Smart-Clean-Execution-Coverage-2026-03-09.md +git commit -m "docs: narrow execution and recovery claims" +``` + +### Task 4: Final Verification and Handoff + +**Files:** +- Review only: `Docs/Execution/Beta-Acceptance-Checklist.md` +- Review only: `Docs/Execution/Execution-Chain-Audit-2026-03-09.md` +- Review only: `Docs/ROADMAP.md` +- Review only: `Docs/Backlog.md` + +**Step 1: Run focused regression commands** + +Run: + +```bash +swift test --package-path Packages --filter AtlasXPCTransportTests +swift test --package-path Packages --filter AtlasApplicationTests +swift test --package-path Apps --filter AtlasAppModelTests +``` + +Expected: + +- all targeted tests pass + +**Step 2: Run broader package and app tests if the focused tests are green** + +Run: + +```bash +swift test --package-path Packages +swift test --package-path Apps +``` + +Expected: + +- no regressions in package or app test suites + +**Step 3: Manual product verification** + +Verify on the latest local packaged or debug build: + +1. Force an execution-unavailable path and confirm Smart Clean shows a visible danger-state failure. +2. Confirm no silent fallback success path is visible in normal app mode. +3. Confirm Smart Clean still executes supported safe targets. +4. Confirm Recovery/History wording no longer implies guaranteed physical restore for every item. + +**Step 4: Handoff notes** + +Record: + +- what was changed +- which files changed +- which tests passed +- whether physical restore remains partial after copy hardening +- whether signed-release work is still blocked by missing credentials diff --git a/Docs/Execution/Release-Roadmap-2026-03-12.md b/Docs/Execution/Release-Roadmap-2026-03-12.md new file mode 100644 index 0000000..a8ac7b8 --- /dev/null +++ b/Docs/Execution/Release-Roadmap-2026-03-12.md @@ -0,0 +1,153 @@ +# Internal Beta Hardening and Conditional Release Roadmap — 2026-03-12 + +## Product Conclusion + +Atlas for Mac should not optimize around public beta dates right now. The correct near-term program is `internal beta hardening`: execution truthfulness first, recovery credibility second, signed public distribution later when Apple release credentials exist. + +This plan assumes: + +- the frozen MVP module list does not change +- direct distribution remains the eventual release route +- no public beta or GA milestone is active until signing credentials are available + +## Starting Evidence + +- `Docs/Execution/Current-Status-2026-03-07.md` marks Atlas as internal-beta ready. +- `Docs/Execution/Beta-Gate-Review.md` passes the beta candidate gate with conditions. +- `Docs/Execution/Execution-Chain-Audit-2026-03-09.md` identifies the two biggest trust gaps: + - silent fallback from XPC to scaffold worker + - partial real execution and non-physical restore behavior +- `Docs/Execution/Release-Signing.md` shows public distribution is blocked by missing Apple release credentials on the current machine. + +## Active Phase Plan + +### Phase 1: Internal Beta Hardening + +- Dates: `2026-03-16` to `2026-03-28` +- Outcome: a truthful internal-beta build that no longer overclaims execution or recovery capability + +#### Workstreams + +- `System Agent` + - remove or development-gate silent XPC fallback in release-facing flows + - surface explicit user-facing errors when real worker execution is unavailable +- `QA Agent` + - rerun clean-machine validation for first launch, language switching, and install flow + - rerun `Smart Clean` and `Apps` end-to-end manual flows against packaged artifacts +- `UX Agent` + `Docs Agent` + - tighten copy where `Recovery` or `Smart Clean` implies broader on-disk behavior than shipped + +#### Exit Gate + +- latest packaged build passes internal beta checklist again +- unsupported execution paths are visible and honest +- recovery language matches the current shipped restore model + +### Phase 2: Smart Clean Execution Credibility + +- Dates: `2026-03-31` to `2026-04-18` +- Outcome: highest-value safe cleanup paths have proven real side effects + +#### Workstreams + +- `System Agent` + `Core Agent` + - expand real `Smart Clean` execute coverage for top safe target classes + - carry executable structured targets through the worker path + - route privileged cleanup through the helper boundary where necessary +- `QA Agent` + - add stronger `scan -> execute -> rescan` contract coverage + - verify history and completion states only claim real success +- `Mac App Agent` + - align completion and failure states with true execution outcomes + +#### Exit Gate + +- supported cleanup paths show real post-execution scan improvement +- unsupported paths fail clearly +- history only claims completion when the filesystem side effect happened + +### Phase 3: Recovery Credibility + +- Dates: `2026-04-21` to `2026-05-09` +- Outcome: Atlas's recovery promise is either physically true for file-backed actions or explicitly narrowed before release planning resumes + +#### Workstreams + +- `System Agent` + - implement physical restore for file-backed recoverable actions where safe + - or freeze a narrower recovery contract if physical restore cannot be landed safely +- `QA Agent` + - validate restore behavior on real file-backed test cases +- `Docs Agent` + `Product Agent` + - freeze README, release-note, and in-app recovery wording only after behavior is confirmed + +#### Exit Gate + +- file-backed recoverable actions either restore physically or are no longer described as if they do +- QA evidence exists for shipped restore behavior +- recovery claims are consistent across docs and product copy + +## Conditional Release Plan + +This branch is dormant until Apple release credentials exist. + +### Conditional Phase A: Signed Public Beta Candidate + +- Trigger: + - `Developer ID Application` + - `Developer ID Installer` + - `ATLAS_NOTARY_PROFILE` +- Workstreams: + - run `./scripts/atlas/signing-preflight.sh` + - rerun signed packaging + - validate signed install behavior on a clean machine + - prepare public beta notes and known limitations +- Exit Gate: + - signed and notarized artifacts exist + - clean-machine install verification passes + +### Conditional Phase B: Public Beta Learn Loop + +- Trigger: + - Conditional Phase A complete +- Workstreams: + - run a small hardware-diverse trusted beta cohort + - triage install, permission, execution, and restore regressions + - close P0 issues before any GA candidate is named +- Exit Gate: + - no public-beta P0 remains open + - primary workflows are validated across more than one machine profile + +### Conditional Phase C: GA Candidate and Launch + +- Trigger: + - Conditional Phase B complete +- Workstreams: + - rerun full acceptance and signed packaging on the GA candidate + - freeze release notes, notices, acknowledgements, and checksums + - validate launch-candidate install and first-run flow on a clean machine +- Exit Gate: + - no open P0 release blocker + - signed packaging, install validation, and release docs are complete + - `v1.0` is published + +## Workstream Priorities + +### Priority 1: Execution Truthfulness + +Atlas cannot afford a release where `Smart Clean` appears to succeed while only Atlas state changes. Silent fallback removal, explicit execution failures, and `scan -> execute -> rescan` proof are the current highest-value work. + +### Priority 2: Recovery Credibility + +`Recovery` is part of Atlas's core trust story. Before broad release planning resumes, Atlas must either ship physical restore for file-backed recoverable actions or narrow its promise to the behavior that actually exists. + +### Priority 3: Signed Distribution When Unblocked + +Signing and notarization matter, but they are not the active critical path until credentials exist. When the credentials arrive, Gatekeeper-safe installation becomes a release gate, not a background task. + +## Decision Rules + +- Do not add P1 modules during this roadmap window. +- Do not call the current branch `public beta`. +- Do not claim broader cleanup coverage than the worker/helper path can prove. +- Do not claim physical recovery until file-backed restore is validated. diff --git a/Docs/Execution/Smart-Clean-Execution-Coverage-2026-03-09.md b/Docs/Execution/Smart-Clean-Execution-Coverage-2026-03-09.md index e250ec0..d97d082 100644 --- a/Docs/Execution/Smart-Clean-Execution-Coverage-2026-03-09.md +++ b/Docs/Execution/Smart-Clean-Execution-Coverage-2026-03-09.md @@ -124,6 +124,7 @@ Use these statements consistently in user-facing communication: - `Smart Clean runs real cleanup only for supported items in the current plan.` - `Unsupported items stay review-only until Atlas can execute them safely.` - `Recoverable items can be restored when a recovery path is available.` +- `Some recoverable items restore on disk, while older or unstructured records restore Atlas state only.` - `If Atlas cannot prove the cleanup step, it should fail instead of claiming success.` Avoid saying: diff --git a/Docs/HELP_CENTER_OUTLINE.md b/Docs/HELP_CENTER_OUTLINE.md index 3b19e9c..1e4edb8 100644 --- a/Docs/HELP_CENTER_OUTLINE.md +++ b/Docs/HELP_CENTER_OUTLINE.md @@ -26,6 +26,7 @@ - Where to find past runs - When recovery restores a file physically +- When recovery only restores Atlas workspace state - Which actions are recoverable - What happens when recovery expires diff --git a/Docs/README.md b/Docs/README.md index 4f65310..09fa975 100644 --- a/Docs/README.md +++ b/Docs/README.md @@ -17,15 +17,17 @@ This directory contains the working product, design, engineering, and compliance - `Protocol.md` — local JSON protocol and core schemas - `TaskStateMachine.md` — task lifecycle rules - `ErrorCodes.md` — user-facing and system error registry -- `ROADMAP.md` — 12-week MVP execution plan +- `ROADMAP.md` — active internal-beta hardening roadmap and conditional release branch - `Backlog.md` — epics, issue seeds, and board conventions - `DECISIONS.md` — frozen product and architecture decisions - `RISKS.md` — active project risk register - `Execution/` — weekly execution plans, status snapshots, beta checklists, gate reviews, manual test SOPs, and release execution notes - `Execution/Current-Status-2026-03-07.md` — current engineering status snapshot +- `Execution/Release-Roadmap-2026-03-12.md` — internal-beta hardening plan plus conditional signed release path - `Execution/UI-Audit-2026-03-08.md` — UI design audit and prioritized remediation directions - `Execution/UI-Copy-Walkthrough-2026-03-09.md` — page-by-page UI copy glossary, consistency checklist, and acceptance guide - `Execution/Execution-Chain-Audit-2026-03-09.md` — end-to-end review of real vs scaffold execution paths and release-facing trust gaps +- `Execution/Implementation-Plan-ATL-201-202-205-2026-03-12.md` — implementation plan for internal-beta hardening tasks ATL-201, ATL-202, and ATL-205 - `Execution/Smart-Clean-Execution-Coverage-2026-03-09.md` — user-facing summary of what Smart Clean can execute for real today - `Execution/Smart-Clean-QA-Checklist-2026-03-09.md` — QA checklist for scan, execute, rescan, and physical restore validation - `Execution/Smart-Clean-Manual-Verification-2026-03-09.md` — local-machine fixture workflow for validating real Smart Clean execution and restore diff --git a/Docs/RISKS.md b/Docs/RISKS.md index 099c965..cd92dc5 100644 --- a/Docs/RISKS.md +++ b/Docs/RISKS.md @@ -46,7 +46,7 @@ - Probability: Medium - Owner: `Release Agent` - Risk: Helper signing or notarization may fail late in the schedule. -- Mitigation: Validate packaging flow before feature-complete milestone. Current repo now includes native build/package scripts and CI workflow, but signing and notarization still depend on release credentials. +- Mitigation: Keep signed distribution off the active critical path until Apple release credentials exist. Once credentials are available, validate packaging flow before any public beta naming or broad external distribution. ## R-007 Experience Polish Drift @@ -96,3 +96,19 @@ - Owner: `System Agent` - Risk: Silent fallback from XPC to the scaffold worker can make user-facing execution appear successful even when the primary worker path is unavailable. - Mitigation: Restrict fallback to explicit development mode or surface a concrete error when real execution infrastructure is unavailable. + +## R-013 Public Beta Coverage Blind Spot + +- Impact: High +- Probability: Medium +- Owner: `QA Agent` +- Risk: When signing credentials eventually arrive, a public beta that is too small, too homogeneous, or too unstructured may miss install, permission, or cleanup regressions that only appear on different hardware, macOS states, or trust settings. +- Mitigation: Keep this as a conditional release risk. Use a deliberately hardware-diverse trusted beta cohort, require structured issue intake, and rerun clean-machine install and first-run validation before calling any signed build GA-ready. + +## R-014 GA Recovery Claim Drift + +- Impact: High +- Probability: Medium +- Owner: `Product Agent` +- Risk: GA release notes, README copy, or in-app messaging may overstate Atlas's recovery model before physical restore is actually shipped for file-backed recoverable actions. +- Mitigation: Treat recovery wording as a gated release artifact. Either ship physical restore for file-backed recoverable actions before GA or narrow all GA-facing recovery claims to the shipped behavior. diff --git a/Docs/ROADMAP.md b/Docs/ROADMAP.md index 18c362c..e06cfdc 100644 --- a/Docs/ROADMAP.md +++ b/Docs/ROADMAP.md @@ -1,59 +1,134 @@ -# MVP Roadmap +# Roadmap -## Timeline +## Current Starting Point -### Week 1 +- Date: `2026-03-12` +- Product state: `Frozen MVP complete` +- Validation state: `Internal beta passed with conditions on 2026-03-07` +- Immediate priorities: + - remove silent XPC fallback from release-facing trust assumptions + - make `Smart Clean` execution honesty match real filesystem behavior + - make `Recovery` claims match shipped restore behavior +- Release-path blocker: + - no Apple signing and notarization credentials are available on the current machine -- Freeze MVP scope -- Freeze naming, compliance, and acknowledgement strategy -- Freeze product goals and success metrics +## Roadmap Guardrails -### Week 2 +- Keep scope inside the frozen MVP modules: + - `Overview` + - `Smart Clean` + - `Apps` + - `History` + - `Recovery` + - `Permissions` + - `Settings` +- Do not pull `Storage treemap`, `Menu Bar`, or `Automation` into this roadmap. +- Treat trust and recovery honesty as release-critical product work, not polish. +- Keep direct distribution as the only eventual release route. +- Do not plan around public beta dates until signing credentials exist. -- Freeze IA and high-fidelity design input for key screens -- Freeze interaction states and permission explainers +## Active Milestones -### Week 3 +### Milestone 1: Internal Beta Hardening -- Freeze architecture, protocol, state machine, and helper boundaries +- Dates: `2026-03-16` to `2026-03-28` +- Goal: harden the current internal-beta build until user-visible execution and recovery claims are defensible. +- Focus: + - remove or explicitly development-gate silent XPC fallback + - show explicit failure states when real worker execution is unavailable + - rerun bilingual manual QA on a clean machine + - verify packaged first-launch behavior with a fresh state file + - tighten README, in-app copy, and help content where recovery or execution is overstated +- Exit criteria: + - internal beta checklist rerun against the latest packaged build + - unsupported execution paths fail clearly instead of appearing successful + - recovery wording matches the shipped restore behavior -### Week 4 +### Milestone 2: Smart Clean Execution Credibility -- Create engineering scaffold and mock-data application shell +- Dates: `2026-03-31` to `2026-04-18` +- Goal: prove that the highest-value safe cleanup paths have real disk-backed side effects. +- Focus: + - expand real `Smart Clean` execute coverage for top safe target classes + - carry executable structured targets through the worker path + - add stronger `scan -> execute -> rescan` contract coverage + - make history and completion states reflect real side effects only +- Exit criteria: + - top safe cleanup paths show real post-execution scan improvement + - history does not claim success without real side effects + - release-facing docs clearly distinguish supported vs unsupported cleanup paths -### Week 5 +### Milestone 3: Recovery Credibility -- Ship scan initiation and result pipeline +- Dates: `2026-04-21` to `2026-05-09` +- Goal: close the gap between Atlas's recovery promise and its shipped restore behavior. +- Focus: + - implement physical restore for file-backed recoverable actions where safe + - or narrow product and release messaging if physical restore cannot land safely + - validate restore behavior on real file-backed test cases + - freeze recovery-related copy only after behavior is confirmed +- Exit criteria: + - recovery language matches shipped behavior + - file-backed recoverable actions either restore physically or are no longer described as if they do + - QA has explicit evidence for restore behavior on the candidate build -### Week 6 +## Conditional Release Branch -- Ship action-plan preview and cleanup execution path +These milestones do not start until Apple release credentials are available. -### Week 7 +### Conditional Milestone A: Signed Public Beta Candidate -- Ship apps list and uninstall preview flow +- Trigger: + - `Developer ID Application` is available + - `Developer ID Installer` is available + - `ATLAS_NOTARY_PROFILE` is available +- Goal: produce a signed and notarized external beta candidate. +- Focus: + - pass `./scripts/atlas/signing-preflight.sh` + - rerun signed packaging + - validate signed `.app`, `.dmg`, and `.pkg` install paths on a clean machine + - prepare public beta notes and known limitations +- Exit criteria: + - signed and notarized artifacts install without bypass instructions + - clean-machine install verification passes on the signed candidate -### Week 8 +### Conditional Milestone B: Public Beta Learn Loop -- Ship permissions center, history, and recovery views +- Trigger: + - Conditional Milestone A is complete +- Goal: run a small external beta after internal hardening is already complete. +- Focus: + - use a hardware-diverse trusted beta cohort + - triage install, permission, execution, and restore regressions + - close P0 issues before any GA candidate is named +- Exit criteria: + - no public-beta P0 remains open + - primary workflows are validated on more than one machine profile -### Week 9 +### Conditional Milestone C: GA Candidate and Launch -- Integrate privileged helper path and audit trail +- Trigger: + - Conditional Milestone B is complete +- Goal: publish `v1.0` only after trust, recovery, and signed distribution all align. +- Focus: + - rerun full acceptance and signed packaging on the GA candidate + - freeze release notes, notices, acknowledgements, and checksums + - validate launch candidate install and first-run flow on a clean machine +- Exit criteria: + - no open P0 release blocker + - signed packaging, install validation, and release docs are complete + - `v1.0` artifacts are published -### Week 10 +## Current Decision Rules -- Run quality, regression, and performance hardening +- Do not call the current workstream `public beta`. +- Do not claim broader cleanup coverage than the worker/helper path can prove. +- Do not claim physical recovery until file-backed restore is actually validated. +- Do not schedule a public release date before signing credentials exist. -### Week 11 +## Not In This Roadmap -- Produce beta candidate and packaging pipeline - -### Week 12 - -- Internal beta wrap-up and release-readiness review - -## MVP Scope - -- In scope: `Overview`, `Smart Clean`, `Apps`, `History`, `Recovery`, `Permissions` -- Deferred to P1: `Storage treemap`, `Menu Bar`, `Automation` +- `Storage treemap` +- `Menu Bar` +- `Automation` +- new non-MVP modules diff --git a/Packages/AtlasApplication/Sources/AtlasApplication/AtlasApplication.swift b/Packages/AtlasApplication/Sources/AtlasApplication/AtlasApplication.swift index 8e08cba..eb97551 100644 --- a/Packages/AtlasApplication/Sources/AtlasApplication/AtlasApplication.swift +++ b/Packages/AtlasApplication/Sources/AtlasApplication/AtlasApplication.swift @@ -143,7 +143,14 @@ public enum AtlasWorkspaceControllerError: LocalizedError, Sendable { public var errorDescription: String? { switch self { case let .rejected(code, reason): - return AtlasL10n.string("application.error.workerRejected", code.rawValue, reason) + switch code { + case .executionUnavailable: + return AtlasL10n.string("application.error.executionUnavailable", reason) + case .helperUnavailable: + return AtlasL10n.string("application.error.helperUnavailable", reason) + default: + return AtlasL10n.string("application.error.workerRejected", code.rawValue, reason) + } case let .unexpectedResponse(reason): return reason } diff --git a/Packages/AtlasApplication/Tests/AtlasApplicationTests/AtlasApplicationTests.swift b/Packages/AtlasApplication/Tests/AtlasApplicationTests/AtlasApplicationTests.swift index 1fe85b1..4668a70 100644 --- a/Packages/AtlasApplication/Tests/AtlasApplicationTests/AtlasApplicationTests.swift +++ b/Packages/AtlasApplication/Tests/AtlasApplicationTests/AtlasApplicationTests.swift @@ -162,6 +162,52 @@ final class AtlasApplicationTests: XCTestCase { XCTAssertEqual(output.snapshot.permissions.count, permissions.count) XCTAssertEqual(output.events.count, permissions.count) } + + func testExecutePlanMapsExecutionUnavailableToLocalizedError() async throws { + let plan = AtlasScaffoldWorkspace.state().currentPlan + let request = AtlasRequestEnvelope(command: .executePlan(planID: plan.id)) + let result = AtlasWorkerCommandResult( + request: request, + response: AtlasResponseEnvelope( + requestID: request.id, + response: .rejected(code: .executionUnavailable, reason: "XPC worker offline") + ), + events: [], + snapshot: AtlasScaffoldWorkspace.snapshot(), + previewPlan: nil + ) + let controller = AtlasWorkspaceController(worker: FakeWorker(result: result)) + + do { + _ = try await controller.executePlan(planID: plan.id) + XCTFail("Expected executePlan to throw") + } catch { + XCTAssertEqual(error.localizedDescription, AtlasL10n.string("application.error.executionUnavailable", "XPC worker offline")) + } + } + + func testRestoreItemsMapsHelperUnavailableToLocalizedError() async throws { + let itemID = UUID() + let request = AtlasRequestEnvelope(command: .restoreItems(taskID: UUID(), itemIDs: [itemID])) + let result = AtlasWorkerCommandResult( + request: request, + response: AtlasResponseEnvelope( + requestID: request.id, + response: .rejected(code: .helperUnavailable, reason: "Privileged helper missing") + ), + events: [], + snapshot: AtlasScaffoldWorkspace.snapshot(), + previewPlan: nil + ) + let controller = AtlasWorkspaceController(worker: FakeWorker(result: result)) + + do { + _ = try await controller.restoreItems(itemIDs: [itemID]) + XCTFail("Expected restoreItems to throw") + } catch { + XCTAssertEqual(error.localizedDescription, AtlasL10n.string("application.error.helperUnavailable", "Privileged helper missing")) + } + } } private actor FakeWorker: AtlasWorkerServing { diff --git a/Packages/AtlasDomain/Sources/AtlasDomain/Resources/en.lproj/Localizable.strings b/Packages/AtlasDomain/Sources/AtlasDomain/Resources/en.lproj/Localizable.strings index ab62de7..619826f 100644 --- a/Packages/AtlasDomain/Sources/AtlasDomain/Resources/en.lproj/Localizable.strings +++ b/Packages/AtlasDomain/Sources/AtlasDomain/Resources/en.lproj/Localizable.strings @@ -90,6 +90,8 @@ "fixture.storage.installers.title" = "Unused installers"; "fixture.storage.installers.age" = "Mostly disk images older than 30 days"; "application.error.workerRejected" = "Worker rejected request (%@): %@"; +"application.error.executionUnavailable" = "Atlas could not run this action with the real worker path: %@"; +"application.error.helperUnavailable" = "Atlas could not complete this action because the privileged helper is unavailable: %@"; "xpc.error.encodingFailed" = "Could not encode the background worker request: %@"; "xpc.error.decodingFailed" = "Could not decode the background worker response: %@"; "xpc.error.invalidResponse" = "The background worker returned an invalid response. Fully quit and reopen Atlas; if it still fails, reinstall the current build."; @@ -103,7 +105,7 @@ "application.preview.updated.one" = "Updated the cleanup plan for 1 action."; "application.preview.updated.other" = "Updated the cleanup plan for %d actions."; "application.plan.executed" = "Cleanup plan completed."; -"application.recovery.completed" = "Recovery restore completed."; +"application.recovery.completed" = "Recovery request completed. On-disk return is available only when Atlas has a supported restore path."; "application.apps.loaded.one" = "Loaded 1 app footprint."; "application.apps.loaded.other" = "Loaded %d app footprints."; "application.apps.previewUpdated" = "Updated the uninstall plan for %@."; @@ -285,13 +287,13 @@ "smartclean.execution.reviewOnly" = "Review Only"; "smartclean.execution.coverage.full" = "All %d steps in this plan can run directly"; "smartclean.execution.coverage.partial" = "Only %d of %d steps in this plan can run directly"; -"smartclean.execution.coverage.full.detail" = "These steps will really move items to Trash and support restore when a recovery path is available."; +"smartclean.execution.coverage.full.detail" = "These steps can really move supported targets to Trash. On-disk restore is available only when Atlas records a matching recovery path."; "smartclean.execution.coverage.partial.detail" = "%d remaining step(s) still need review or are not supported for direct execution yet."; "smartclean.preview.metric.space.title" = "This Plan Can Free"; "smartclean.preview.metric.space.detail.one" = "Estimated across 1 planned step."; "smartclean.preview.metric.space.detail.other" = "Estimated across %d planned steps."; "smartclean.preview.callout.safe.title" = "This plan stays mostly in the Safe lane"; -"smartclean.preview.callout.safe.detail" = "Most selected steps should remain recoverable through History and Recovery."; +"smartclean.preview.callout.safe.detail" = "Most selected steps should remain reviewable through History and Recovery. Physical restore is available only for items with a supported recovery path."; "smartclean.preview.empty.detail.postExecution" = "Run a scan or update the plan to turn current findings into concrete cleanup steps. If you just ran a plan, this section shows only the remaining items."; "smartclean.preview.callout.review.detail" = "Check the highlighted steps before you run the plan so you understand what stays recoverable and what needs extra judgment."; "smartclean.preview.empty.title" = "No cleanup plan yet"; @@ -303,6 +305,7 @@ "smartclean.status.empty" = "Run a fresh scan to generate a cleanup plan"; "smartclean.status.cached" = "This plan has not been revalidated yet"; "smartclean.status.revalidationFailed" = "Could not update the current plan"; +"smartclean.status.executionFailed" = "Could not run the current plan"; "smartclean.cached.title" = "This plan is from a previous result"; "smartclean.revalidationFailed.title" = "Revalidation failed, so this still shows the previous plan"; "smartclean.cached.detail" = "Run a fresh scan or update the plan before you execute it. What you see here is the last saved plan, not a verified current result."; @@ -390,7 +393,7 @@ "history.callout.running.title" = "A recent task is still in progress"; "history.callout.running.detail" = "Keep the timeline open if you want to confirm when it finishes and whether it creates new recovery items."; "history.callout.recovery.title" = "Recovery-first cleanup is active"; -"history.callout.recovery.detail" = "Each recoverable action stays visible until its retention window ends, so you can reverse decisions with confidence."; +"history.callout.recovery.detail" = "Each recoverable action stays visible until its retention window ends. Some items can return on disk; others only restore Atlas's workspace state."; "history.metric.activity.title" = "Visible events"; "history.metric.activity.detail.empty" = "Run a scan or cleanup action to build the audit trail."; "history.metric.activity.detail.latest" = "Latest update %@"; @@ -459,12 +462,12 @@ "history.detail.recovery.window" = "Retention window"; "history.detail.recovery.window.open" = "Still recoverable"; "history.detail.recovery.callout.available.title" = "This item is still recoverable"; -"history.detail.recovery.callout.available.detail" = "Restore it whenever you are ready while the retention window remains open."; +"history.detail.recovery.callout.available.detail" = "Restore it while the retention window remains open. On-disk return is available only when Atlas has a supported restore path for this item."; "history.detail.recovery.callout.expiring.title" = "Restore soon if you still need this"; "history.detail.recovery.callout.expiring.detail" = "This recovery item is close to the end of its retention window."; "history.restore.action" = "Restore"; "history.restore.running" = "Restoring…"; -"history.restore.hint" = "Restores this item while its recovery window is still open."; +"history.restore.hint" = "Restores this item while its recovery window is still open. On-disk return is available only for supported file-backed items."; "history.run.footnote.finished" = "Started %@ • Finished %@"; "history.run.footnote.running" = "Started %@ • Still in progress"; "history.recovery.footnote.deleted" = "Deleted %@"; @@ -573,7 +576,7 @@ "confirm.title" = "Confirm"; "confirm.cancel" = "Cancel"; "smartclean.confirm.execute.title" = "Run this cleanup plan?"; -"smartclean.confirm.execute.message" = "This will process the reviewed plan. Items marked recoverable can be restored from History."; +"smartclean.confirm.execute.message" = "This will process the reviewed plan. Recoverable items stay visible in History, and on-disk restore is available only when Atlas has a supported recovery path."; "apps.confirm.uninstall.title" = "Uninstall this app?"; "apps.confirm.uninstall.message" = "This will remove %@ and its leftovers. Recoverable items can be restored from History."; "emptystate.action.scan" = "Run Smart Clean"; diff --git a/Packages/AtlasDomain/Sources/AtlasDomain/Resources/zh-Hans.lproj/Localizable.strings b/Packages/AtlasDomain/Sources/AtlasDomain/Resources/zh-Hans.lproj/Localizable.strings index ef492d0..b972aca 100644 --- a/Packages/AtlasDomain/Sources/AtlasDomain/Resources/zh-Hans.lproj/Localizable.strings +++ b/Packages/AtlasDomain/Sources/AtlasDomain/Resources/zh-Hans.lproj/Localizable.strings @@ -90,6 +90,8 @@ "fixture.storage.installers.title" = "未使用的安装器"; "fixture.storage.installers.age" = "大部分磁盘镜像已超过 30 天"; "application.error.workerRejected" = "后台服务拒绝了请求(%@):%@"; +"application.error.executionUnavailable" = "Atlas 当前无法通过真实工作链路执行这项操作:%@"; +"application.error.helperUnavailable" = "Atlas 当前无法完成这项操作,因为特权辅助组件不可用:%@"; "xpc.error.encodingFailed" = "无法编码后台请求:%@"; "xpc.error.decodingFailed" = "无法解析后台响应:%@"; "xpc.error.invalidResponse" = "后台工作组件返回了无效响应。请完全退出并重新打开 Atlas;若仍失败,请重新安装当前版本。"; @@ -103,7 +105,7 @@ "application.preview.updated.one" = "已根据 1 个操作更新清理计划。"; "application.preview.updated.other" = "已根据 %d 个操作更新清理计划。"; "application.plan.executed" = "清理计划已执行完成。"; -"application.recovery.completed" = "恢复操作已完成。"; +"application.recovery.completed" = "恢复请求已完成。只有在 Atlas 具备受支持恢复路径时,项目才会真正回到磁盘。"; "application.apps.loaded.one" = "已载入 1 个应用占用项。"; "application.apps.loaded.other" = "已载入 %d 个应用占用项。"; "application.apps.previewUpdated" = "已更新 %@ 的卸载计划。"; @@ -285,13 +287,13 @@ "smartclean.execution.reviewOnly" = "仅供复核"; "smartclean.execution.coverage.full" = "这份计划中的 %d 个步骤都可直接执行"; "smartclean.execution.coverage.partial" = "这份计划中有 %d/%d 个步骤可直接执行"; -"smartclean.execution.coverage.full.detail" = "这些步骤会真正移动到废纸篓,并在有恢复路径时支持恢复。"; +"smartclean.execution.coverage.full.detail" = "这些步骤会真正把受支持的目标移到废纸篓。只有在 Atlas 记录了匹配恢复路径时,才支持磁盘级恢复。"; "smartclean.execution.coverage.partial.detail" = "其余 %d 个步骤仍需复核,或当前还不支持直接执行。"; "smartclean.preview.metric.space.title" = "这份计划预计释放"; "smartclean.preview.metric.space.detail.one" = "按当前 1 个计划步骤估算。"; "smartclean.preview.metric.space.detail.other" = "按当前 %d 个计划步骤估算。"; "smartclean.preview.callout.safe.title" = "当前计划主要来自“安全”分区"; -"smartclean.preview.callout.safe.detail" = "大多数已选步骤都可以在历史和恢复中找回。"; +"smartclean.preview.callout.safe.detail" = "大多数已选步骤都会在历史和恢复中保留可追溯记录。只有具备受支持恢复路径的项目,才支持磁盘级恢复。"; "smartclean.preview.empty.detail.postExecution" = "运行一次扫描或更新计划,把当前发现项变成具体的清理步骤。若刚执行完计划,这里只显示剩余项目。"; "smartclean.preview.callout.review.detail" = "建议在执行前检查高亮步骤,确认哪些仍可恢复、哪些需要额外判断。"; "smartclean.preview.empty.title" = "还没有清理计划"; @@ -303,6 +305,7 @@ "smartclean.status.empty" = "运行新的扫描以生成清理计划"; "smartclean.status.cached" = "当前计划尚未重新验证"; "smartclean.status.revalidationFailed" = "未能更新当前计划"; +"smartclean.status.executionFailed" = "当前计划未能执行"; "smartclean.cached.title" = "这份计划来自上一次结果"; "smartclean.revalidationFailed.title" = "重新验证失败,以下仍是上一次计划"; "smartclean.cached.detail" = "请先重新运行扫描或更新计划,再执行。当前显示的只是上一次保存的计划,不能直接视为当前可执行结果。"; @@ -390,7 +393,7 @@ "history.callout.running.title" = "最近有任务仍在进行中"; "history.callout.running.detail" = "保留时间线可帮助你确认任务何时完成,以及是否会产生新的可恢复项目。"; "history.callout.recovery.title" = "恢复优先的清理策略已启用"; -"history.callout.recovery.detail" = "每个可恢复操作都会在保留期结束前保持可见,便于你有把握地撤销决定。"; +"history.callout.recovery.detail" = "每个可恢复操作都会在保留期结束前保持可见。有些项目可以恢复回磁盘,有些则只会恢复 Atlas 的工作区状态。"; "history.metric.activity.title" = "当前记录"; "history.metric.activity.detail.empty" = "运行一次扫描或清理操作后,这里会开始形成审计轨迹。"; "history.metric.activity.detail.latest" = "最近更新 %@"; @@ -459,12 +462,12 @@ "history.detail.recovery.window" = "保留窗口"; "history.detail.recovery.window.open" = "仍可恢复"; "history.detail.recovery.callout.available.title" = "这个项目仍可恢复"; -"history.detail.recovery.callout.available.detail" = "只要保留窗口还在,你可以在准备好后随时恢复。"; +"history.detail.recovery.callout.available.detail" = "只要保留窗口还在,你就可以恢复。只有当 Atlas 为该项目记录了受支持的恢复路径时,它才会真正回到磁盘。"; "history.detail.recovery.callout.expiring.title" = "如果还需要它,请尽快恢复"; "history.detail.recovery.callout.expiring.detail" = "这个恢复项已经接近保留窗口的结束时间。"; "history.restore.action" = "恢复"; "history.restore.running" = "正在恢复…"; -"history.restore.hint" = "只要恢复窗口仍然开放,就可以把这个项目恢复回工作区。"; +"history.restore.hint" = "只要恢复窗口仍然开放,就可以恢复这个项目。只有受支持的文件型项目才会真正回到磁盘。"; "history.run.footnote.finished" = "开始于 %@ • 结束于 %@"; "history.run.footnote.running" = "开始于 %@ • 仍在进行中"; "history.recovery.footnote.deleted" = "删除于 %@"; @@ -573,7 +576,7 @@ "confirm.title" = "确认"; "confirm.cancel" = "取消"; "smartclean.confirm.execute.title" = "执行这份清理计划?"; -"smartclean.confirm.execute.message" = "将按复核后的计划执行清理。标记为可恢复的项目可以在历史中找回。"; +"smartclean.confirm.execute.message" = "将按复核后的计划执行清理。可恢复项目会保留在历史中;只有具备受支持恢复路径的项目,才支持磁盘级恢复。"; "apps.confirm.uninstall.title" = "卸载这个应用?"; "apps.confirm.uninstall.message" = "将移除 %@ 及其残留文件。可恢复的项目可以在历史中找回。"; "emptystate.action.scan" = "运行智能清理"; diff --git a/Packages/AtlasFeaturesSmartClean/Sources/AtlasFeaturesSmartClean/SmartCleanFeatureView.swift b/Packages/AtlasFeaturesSmartClean/Sources/AtlasFeaturesSmartClean/SmartCleanFeatureView.swift index 4eb2027..f7564f0 100644 --- a/Packages/AtlasFeaturesSmartClean/Sources/AtlasFeaturesSmartClean/SmartCleanFeatureView.swift +++ b/Packages/AtlasFeaturesSmartClean/Sources/AtlasFeaturesSmartClean/SmartCleanFeatureView.swift @@ -14,6 +14,7 @@ public struct SmartCleanFeatureView: View { private let isCurrentPlanFresh: Bool private let canExecutePlan: Bool private let planIssue: String? + private let executionIssue: String? private let onStartScan: () -> Void private let onRefreshPreview: () -> Void private let onExecutePlan: () -> Void @@ -30,6 +31,7 @@ public struct SmartCleanFeatureView: View { isCurrentPlanFresh: Bool = false, canExecutePlan: Bool = false, planIssue: String? = nil, + executionIssue: String? = nil, onStartScan: @escaping () -> Void = {}, onRefreshPreview: @escaping () -> Void = {}, onExecutePlan: @escaping () -> Void = {} @@ -43,6 +45,7 @@ public struct SmartCleanFeatureView: View { self.isCurrentPlanFresh = isCurrentPlanFresh self.canExecutePlan = canExecutePlan self.planIssue = planIssue + self.executionIssue = executionIssue self.onStartScan = onStartScan self.onRefreshPreview = onRefreshPreview self.onExecutePlan = onExecutePlan @@ -154,7 +157,7 @@ public struct SmartCleanFeatureView: View { ) } - if !plan.items.isEmpty { + if !plan.items.isEmpty, !hasExecutionFailure { AtlasCallout( title: planValidationCalloutTitle, detail: planValidationCalloutDetail, @@ -163,7 +166,7 @@ public struct SmartCleanFeatureView: View { ) } - if plan.items.isEmpty || manualReviewCount > 0 { + if !hasExecutionFailure && (plan.items.isEmpty || manualReviewCount > 0) { AtlasCallout( title: manualReviewCount == 0 ? AtlasL10n.string("smartclean.preview.callout.safe.title") : AtlasL10n.string("smartclean.preview.callout.review.title"), detail: manualReviewCount == 0 @@ -317,6 +320,10 @@ public struct SmartCleanFeatureView: View { !isCurrentPlanFresh && planIssue != nil } + private var hasExecutionFailure: Bool { + executionIssue != nil + } + private var isShowingCachedPlanState: Bool { !isCurrentPlanFresh && !plan.items.isEmpty } @@ -355,6 +362,7 @@ public struct SmartCleanFeatureView: View { private var statusTitle: String { if isScanning { return AtlasL10n.string("smartclean.status.scanning") } if isExecutingPlan { return AtlasL10n.string("smartclean.status.executing") } + if hasExecutionFailure { return AtlasL10n.string("smartclean.status.executionFailed") } if hasPlanRevalidationFailure { return AtlasL10n.string("smartclean.status.revalidationFailed") } if isShowingCachedPlanState { return AtlasL10n.string("smartclean.status.cached") } if findings.isEmpty { return AtlasL10n.string("smartclean.status.empty") } @@ -363,6 +371,7 @@ public struct SmartCleanFeatureView: View { private var statusDetail: String { if isScanning || isExecutingPlan { return scanSummary } + if hasExecutionFailure { return executionIssue ?? scanSummary } if hasPlanRevalidationFailure { return planIssue ?? AtlasL10n.string("smartclean.cached.detail") } if isShowingCachedPlanState { return AtlasL10n.string("smartclean.cached.detail") } if findings.isEmpty { return AtlasL10n.string("smartclean.status.empty.detail") } @@ -372,6 +381,7 @@ public struct SmartCleanFeatureView: View { private var statusTone: AtlasTone { if isExecutingPlan { return .warning } if isScanning { return .neutral } + if hasExecutionFailure { return .danger } if hasPlanRevalidationFailure { return .danger } if isShowingCachedPlanState { return .warning } return manualReviewCount == 0 ? .success : .warning @@ -380,6 +390,7 @@ public struct SmartCleanFeatureView: View { private var statusSymbol: String { if isScanning { return "sparkles" } if isExecutingPlan { return "play.circle.fill" } + if hasExecutionFailure { return "xmark.octagon.fill" } if hasPlanRevalidationFailure { return "xmark.octagon.fill" } if isShowingCachedPlanState { return "externaldrive.badge.exclamationmark" } return manualReviewCount == 0 ? "checkmark.shield.fill" : "exclamationmark.triangle.fill" diff --git a/Packages/AtlasInfrastructure/Tests/AtlasInfrastructureTests/AtlasXPCTransportTests.swift b/Packages/AtlasInfrastructure/Tests/AtlasInfrastructureTests/AtlasXPCTransportTests.swift index 1607fdb..928ad5b 100644 --- a/Packages/AtlasInfrastructure/Tests/AtlasInfrastructureTests/AtlasXPCTransportTests.swift +++ b/Packages/AtlasInfrastructure/Tests/AtlasInfrastructureTests/AtlasXPCTransportTests.swift @@ -60,6 +60,35 @@ final class AtlasXPCTransportTests: XCTestCase { } } + func testPreferredWorkerServiceDoesNotFallbackWhenXPCRejectsExecutionUnavailable() async throws { + let request = AtlasRequestEnvelope(command: .healthSnapshot) + let rejected = AtlasWorkerCommandResult( + request: request, + response: AtlasResponseEnvelope( + requestID: request.id, + response: .rejected(code: .executionUnavailable, reason: "simulated packaged worker failure") + ), + events: [], + snapshot: AtlasScaffoldWorkspace.snapshot(), + previewPlan: nil + ) + let responseData = try JSONEncoder().encode(rejected) + let service = AtlasPreferredWorkerService( + requestConfiguration: AtlasXPCRequestConfiguration(timeout: 1, retryCount: 0, retryDelay: 0), + requestExecutor: { _ in responseData }, + fallbackWorker: AtlasScaffoldWorkerService(allowStateOnlyCleanExecution: true), + allowFallback: false + ) + + let result = try await service.submit(request) + + guard case let .rejected(code, reason) = result.response.response else { + return XCTFail("Expected rejected response without fallback, got \(result.response.response)") + } + XCTAssertEqual(code, .executionUnavailable) + XCTAssertEqual(reason, "simulated packaged worker failure") + } + func testPreferredWorkerServiceFallsBackWhenXPCWorkerRejectsExecutionUnavailable() async throws { let request = AtlasRequestEnvelope(command: .healthSnapshot) diff --git a/README.md b/README.md index af7357f..58ccca5 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@ This repository is the working source for the new Atlas for Mac product. Atlas f ## Disclaimer -Atlas for Mac is an independent open-source project. It is not affiliated with, endorsed by, or sponsored by Apple, the upstream Mole authors, or any other commercial Mac utility vendor. Some components in this repository may reuse or adapt upstream Mole code under the MIT License; when such code ships, the related attribution and third-party notices must remain available. Cleanup, uninstall, and recovery actions can affect local files, caches, and app data, so review findings and recovery options before execution. +Atlas for Mac is an independent open-source project. It is not affiliated with, endorsed by, or sponsored by Apple, the upstream Mole authors, or any other commercial Mac utility vendor. Some components in this repository may reuse or adapt upstream Mole code under the MIT License; when such code ships, the related attribution and third-party notices must remain available. Cleanup, uninstall, and recovery actions can affect local files, caches, and app data, so review findings and recovery options before execution. Recoverable actions remain reviewable in Atlas, but physical on-disk restore is only available when a supported recovery path exists. ## Installation @@ -67,6 +67,7 @@ open Atlas.xcodeproj - Explain recommendations before execution. - Prefer recovery-backed actions over permanent deletion. +- Keep recovery claims honest: not every recoverable item is physically restorable on disk. - Keep permission requests least-privilege and contextual. - Preserve a native macOS app shell with worker and helper boundaries. - Support `简体中文` and `English`, with `简体中文` as the default app language. diff --git a/README.zh-CN.md b/README.zh-CN.md index 2a603e2..025ff15 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -18,7 +18,7 @@ Atlas for Mac 是一款原生 macOS 应用,面向需要弄清楚 Mac 为什么 ## 免责声明 -Atlas for Mac 是一个独立的开源项目,与 Apple、Mole 上游作者或其他商业 Mac 工具厂商不存在隶属、赞助或背书关系。此仓库中的部分组件可能会在 MIT License 下复用或改造上游 Mole 代码;如果这些代码随产品一起发布,相关归因和第三方许可声明必须保留。清理、卸载和恢复类操作可能影响本地文件、缓存和应用数据,因此在执行前请先检查发现结果和恢复选项。 +Atlas for Mac 是一个独立的开源项目,与 Apple、Mole 上游作者或其他商业 Mac 工具厂商不存在隶属、赞助或背书关系。此仓库中的部分组件可能会在 MIT License 下复用或改造上游 Mole 代码;如果这些代码随产品一起发布,相关归因和第三方许可声明必须保留。清理、卸载和恢复类操作可能影响本地文件、缓存和应用数据,因此在执行前请先检查发现结果和恢复选项。可恢复操作仍会在 Atlas 中保留可追溯记录,但只有具备受支持恢复路径的项目,才支持磁盘级恢复。 ## 安装 @@ -67,6 +67,7 @@ open Atlas.xcodeproj - 执行前先解释推荐原因。 - 优先提供可恢复的操作,而不是永久删除。 +- 对恢复能力保持诚实:并非每个“可恢复”项目都能真正回到磁盘。 - 权限请求遵循最小权限并结合具体上下文。 - 保持原生 macOS 应用外壳,并明确 worker 与 helper 边界。 - 同时支持 `简体中文` 和 `English`,其中 `简体中文` 为应用默认语言。