@logictan/dsh-config-manager 0.1.60
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +105 -0
- package/cordis.patch.yml +21 -0
- package/lib/adapters/agent-instructions.d.ts +13 -0
- package/lib/adapters/agent-instructions.js +46 -0
- package/lib/adapters/agent-presets.d.ts +13 -0
- package/lib/adapters/agent-presets.js +14 -0
- package/lib/adapters/credentials.d.ts +37 -0
- package/lib/adapters/credentials.js +122 -0
- package/lib/adapters/file-collection.d.ts +15 -0
- package/lib/adapters/file-collection.js +127 -0
- package/lib/adapters/index.d.ts +57 -0
- package/lib/adapters/index.js +55 -0
- package/lib/adapters/link-report.d.ts +16 -0
- package/lib/adapters/link-report.js +47 -0
- package/lib/adapters/mcp.d.ts +28 -0
- package/lib/adapters/mcp.js +178 -0
- package/lib/adapters/plugin-files.d.ts +18 -0
- package/lib/adapters/plugin-files.js +142 -0
- package/lib/adapters/plugins.d.ts +41 -0
- package/lib/adapters/plugins.js +603 -0
- package/lib/adapters/pnpm-workspace.d.ts +26 -0
- package/lib/adapters/pnpm-workspace.js +147 -0
- package/lib/adapters/prompts.d.ts +30 -0
- package/lib/adapters/prompts.js +231 -0
- package/lib/adapters/providers.d.ts +34 -0
- package/lib/adapters/providers.js +155 -0
- package/lib/adapters/self.d.ts +17 -0
- package/lib/adapters/self.js +71 -0
- package/lib/adapters/sessions.d.ts +14 -0
- package/lib/adapters/sessions.js +15 -0
- package/lib/adapters/settings.d.ts +28 -0
- package/lib/adapters/settings.js +165 -0
- package/lib/adapters/skills.d.ts +13 -0
- package/lib/adapters/skills.js +14 -0
- package/lib/adapters/test-helpers.d.ts +125 -0
- package/lib/adapters/test-helpers.js +236 -0
- package/lib/adapters/ui.d.ts +27 -0
- package/lib/adapters/ui.js +89 -0
- package/lib/adapters/workspaces.d.ts +13 -0
- package/lib/adapters/workspaces.js +106 -0
- package/lib/client.d.ts +1026 -0
- package/lib/client.js +10047 -0
- package/lib/core/analyzer.d.ts +99 -0
- package/lib/core/analyzer.js +934 -0
- package/lib/core/backup.d.ts +138 -0
- package/lib/core/backup.js +513 -0
- package/lib/core/boot-rescue.d.ts +146 -0
- package/lib/core/boot-rescue.js +618 -0
- package/lib/core/cache-cleaner.d.ts +31 -0
- package/lib/core/cache-cleaner.js +95 -0
- package/lib/core/config-lifecycle.d.ts +180 -0
- package/lib/core/config-lifecycle.js +481 -0
- package/lib/core/config-snapshot.d.ts +135 -0
- package/lib/core/config-snapshot.js +424 -0
- package/lib/core/config-state.d.ts +70 -0
- package/lib/core/config-state.js +177 -0
- package/lib/core/consult-source.d.ts +58 -0
- package/lib/core/consult-source.js +260 -0
- package/lib/core/crash-report.d.ts +84 -0
- package/lib/core/crash-report.js +317 -0
- package/lib/core/exporter.d.ts +68 -0
- package/lib/core/exporter.js +375 -0
- package/lib/core/ghost-sweep.d.ts +19 -0
- package/lib/core/ghost-sweep.js +52 -0
- package/lib/core/importer.d.ts +60 -0
- package/lib/core/importer.js +29 -0
- package/lib/core/index.d.ts +14 -0
- package/lib/core/index.js +14 -0
- package/lib/core/journal.d.ts +197 -0
- package/lib/core/journal.js +418 -0
- package/lib/core/local-plugin-host.d.ts +35 -0
- package/lib/core/local-plugin-host.js +120 -0
- package/lib/core/local-plugin-pack.d.ts +135 -0
- package/lib/core/local-plugin-pack.js +278 -0
- package/lib/core/messages.d.ts +310 -0
- package/lib/core/messages.js +621 -0
- package/lib/core/migration-consult.d.ts +117 -0
- package/lib/core/migration-consult.js +363 -0
- package/lib/core/migration-history.d.ts +153 -0
- package/lib/core/migration-history.js +411 -0
- package/lib/core/model-tools.d.ts +50 -0
- package/lib/core/model-tools.js +148 -0
- package/lib/core/msg-types.d.ts +8 -0
- package/lib/core/msg-types.js +6 -0
- package/lib/core/phase3-child-crash.d.ts +1 -0
- package/lib/core/phase3-child-crash.js +87 -0
- package/lib/core/phase3-host.d.ts +138 -0
- package/lib/core/phase3-host.js +359 -0
- package/lib/core/phase3-prod-child.d.ts +1 -0
- package/lib/core/phase3-prod-child.js +35 -0
- package/lib/core/phase4-crash-child.d.ts +1 -0
- package/lib/core/phase4-crash-child.js +103 -0
- package/lib/core/plugin-cli.d.ts +150 -0
- package/lib/core/plugin-cli.js +497 -0
- package/lib/core/reconcile.d.ts +120 -0
- package/lib/core/reconcile.js +389 -0
- package/lib/core/recovery-orchestrator.d.ts +94 -0
- package/lib/core/recovery-orchestrator.js +327 -0
- package/lib/core/restore.d.ts +162 -0
- package/lib/core/restore.js +703 -0
- package/lib/core/rollback.d.ts +12 -0
- package/lib/core/rollback.js +145 -0
- package/lib/core/run-registry.d.ts +85 -0
- package/lib/core/run-registry.js +157 -0
- package/lib/core/startup-barrier.d.ts +53 -0
- package/lib/core/startup-barrier.js +77 -0
- package/lib/core/types.d.ts +515 -0
- package/lib/core/types.js +27 -0
- package/lib/core/undo.d.ts +73 -0
- package/lib/core/undo.js +87 -0
- package/lib/core/validator.d.ts +18 -0
- package/lib/core/validator.js +77 -0
- package/lib/core/verify-recovery.d.ts +21 -0
- package/lib/core/verify-recovery.js +256 -0
- package/lib/core/watcher.d.ts +101 -0
- package/lib/core/watcher.js +219 -0
- package/lib/index.d.ts +238 -0
- package/lib/index.js +2764 -0
- package/lib/migrations/index.d.ts +29 -0
- package/lib/migrations/index.js +56 -0
- package/lib/migrations/v1-to-v2.d.ts +9 -0
- package/lib/migrations/v1-to-v2.js +10 -0
- package/lib/profiles/index.d.ts +4 -0
- package/lib/profiles/index.js +5 -0
- package/lib/profiles/profile-manager.d.ts +106 -0
- package/lib/profiles/profile-manager.js +543 -0
- package/lib/schema/config.d.ts +28 -0
- package/lib/schema/config.js +165 -0
- package/lib/schema/index.d.ts +24 -0
- package/lib/schema/index.js +26 -0
- package/lib/schema/manifest.d.ts +28 -0
- package/lib/schema/manifest.js +109 -0
- package/lib/schema/tombstones.d.ts +46 -0
- package/lib/schema/tombstones.js +64 -0
- package/lib/schema/types.d.ts +232 -0
- package/lib/schema/types.js +8 -0
- package/lib/schema/versions.d.ts +27 -0
- package/lib/schema/versions.js +50 -0
- package/lib/security/encryption.d.ts +85 -0
- package/lib/security/encryption.js +279 -0
- package/lib/security/index.d.ts +16 -0
- package/lib/security/index.js +17 -0
- package/lib/security/integrity.d.ts +30 -0
- package/lib/security/integrity.js +78 -0
- package/lib/security/redaction.d.ts +26 -0
- package/lib/security/redaction.js +95 -0
- package/lib/security/secret-scanner.d.ts +86 -0
- package/lib/security/secret-scanner.js +404 -0
- package/lib/security/vault.d.ts +59 -0
- package/lib/security/vault.js +151 -0
- package/lib/security/zip-security.d.ts +27 -0
- package/lib/security/zip-security.js +154 -0
- package/lib/sync/autosync-config.d.ts +39 -0
- package/lib/sync/autosync-config.js +151 -0
- package/lib/sync/autosync-scheduler.d.ts +139 -0
- package/lib/sync/autosync-scheduler.js +564 -0
- package/lib/sync/fs.d.ts +18 -0
- package/lib/sync/fs.js +49 -0
- package/lib/sync/git/git-transport.d.ts +100 -0
- package/lib/sync/git/git-transport.js +437 -0
- package/lib/sync/github-auth.d.ts +131 -0
- package/lib/sync/github-auth.js +264 -0
- package/lib/sync/layout.d.ts +43 -0
- package/lib/sync/layout.js +203 -0
- package/lib/sync/snapshot-json.d.ts +9 -0
- package/lib/sync/snapshot-json.js +107 -0
- package/lib/sync/sync-config.d.ts +81 -0
- package/lib/sync/sync-config.js +281 -0
- package/lib/sync/sync-engine.d.ts +269 -0
- package/lib/sync/sync-engine.js +591 -0
- package/lib/sync/sync-history.d.ts +42 -0
- package/lib/sync/sync-history.js +78 -0
- package/lib/sync/sync-selection.d.ts +29 -0
- package/lib/sync/sync-selection.js +127 -0
- package/lib/sync/sync-session.d.ts +40 -0
- package/lib/sync/sync-session.js +54 -0
- package/lib/sync/sync-state.d.ts +37 -0
- package/lib/sync/sync-state.js +108 -0
- package/lib/sync/transport.d.ts +75 -0
- package/lib/sync/transport.js +55 -0
- package/lib/sync/ui-prefs.d.ts +34 -0
- package/lib/sync/ui-prefs.js +109 -0
- package/lib/sync/webdav/webdav-transport.d.ts +77 -0
- package/lib/sync/webdav/webdav-transport.js +451 -0
- package/lib/ui/errors.d.ts +19 -0
- package/lib/ui/errors.js +81 -0
- package/lib/ui/export-flow.d.ts +82 -0
- package/lib/ui/export-flow.js +98 -0
- package/lib/ui/i18n.d.ts +289 -0
- package/lib/ui/i18n.js +581 -0
- package/lib/ui/migration-consult-view.d.ts +91 -0
- package/lib/ui/migration-consult-view.js +115 -0
- package/lib/ui/progress.d.ts +38 -0
- package/lib/ui/progress.js +87 -0
- package/lib/ui/report.d.ts +23 -0
- package/lib/ui/report.js +153 -0
- package/lib/ui/test-helpers.d.ts +69 -0
- package/lib/ui/test-helpers.js +154 -0
- package/lib/ui/types.d.ts +257 -0
- package/lib/ui/types.js +21 -0
- package/lib/utils/atomic-write.d.ts +88 -0
- package/lib/utils/atomic-write.js +399 -0
- package/lib/utils/env-lock.d.ts +393 -0
- package/lib/utils/env-lock.js +930 -0
- package/lib/utils/hashing.d.ts +17 -0
- package/lib/utils/hashing.js +41 -0
- package/lib/utils/json.d.ts +24 -0
- package/lib/utils/json.js +76 -0
- package/lib/utils/logger.d.ts +39 -0
- package/lib/utils/logger.js +82 -0
- package/lib/utils/paths.d.ts +42 -0
- package/lib/utils/paths.js +189 -0
- package/lib/utils/proxy.d.ts +69 -0
- package/lib/utils/proxy.js +455 -0
- package/lib/utils/recursive-walk.d.ts +24 -0
- package/lib/utils/recursive-walk.js +134 -0
- package/lib/utils/zip.d.ts +54 -0
- package/lib/utils/zip.js +285 -0
- package/package.json +188 -0
- package/src/adapters/agent-instructions.ts +48 -0
- package/src/adapters/agent-presets.ts +14 -0
- package/src/adapters/credentials.test.ts +90 -0
- package/src/adapters/credentials.ts +143 -0
- package/src/adapters/file-collection.ts +137 -0
- package/src/adapters/files.test.ts +353 -0
- package/src/adapters/index.ts +94 -0
- package/src/adapters/link-report.test.ts +65 -0
- package/src/adapters/link-report.ts +61 -0
- package/src/adapters/mcp.test.ts +96 -0
- package/src/adapters/mcp.ts +185 -0
- package/src/adapters/plugin-files.ts +146 -0
- package/src/adapters/plugins.test.ts +326 -0
- package/src/adapters/plugins.ts +615 -0
- package/src/adapters/pnpm-workspace.test.ts +87 -0
- package/src/adapters/pnpm-workspace.ts +150 -0
- package/src/adapters/prompts.test.ts +110 -0
- package/src/adapters/prompts.ts +230 -0
- package/src/adapters/providers.test.ts +126 -0
- package/src/adapters/providers.ts +173 -0
- package/src/adapters/roundtrip.test.ts +190 -0
- package/src/adapters/self.test.ts +92 -0
- package/src/adapters/self.ts +75 -0
- package/src/adapters/sessions.ts +15 -0
- package/src/adapters/settings.test.ts +100 -0
- package/src/adapters/settings.ts +187 -0
- package/src/adapters/skills.ts +14 -0
- package/src/adapters/test-helpers.ts +248 -0
- package/src/adapters/ui.test.ts +57 -0
- package/src/adapters/ui.ts +108 -0
- package/src/adapters/workspaces.test.ts +106 -0
- package/src/adapters/workspaces.ts +109 -0
- package/src/client/ConfigManagerSection.tsx +90 -0
- package/src/client/api.ts +66 -0
- package/src/client/client-types.ts +18 -0
- package/src/client/common/ErrorBanner.tsx +73 -0
- package/src/client/common/Icon.tsx +129 -0
- package/src/client/common/Modal.tsx +173 -0
- package/src/client/common/ToastViewport.tsx +73 -0
- package/src/client/common/toast-store.test.ts +192 -0
- package/src/client/common/toast-store.ts +181 -0
- package/src/client/common/toast-types.ts +7 -0
- package/src/client/common/ui.tsx +342 -0
- package/src/client/config-manager.module.css +2971 -0
- package/src/client/css-modules.d.ts +8 -0
- package/src/client/index.ts +65 -0
- package/src/client/locales.ts +56 -0
- package/src/client/lucide-icons.d.ts +38 -0
- package/src/client/run-store.test.ts +223 -0
- package/src/client/run-store.ts +372 -0
- package/src/client/sync/SyncConfirmView.tsx +379 -0
- package/src/client/sync/SyncHistoryView.test.ts +40 -0
- package/src/client/sync/SyncHistoryView.tsx +220 -0
- package/src/client/sync/SyncSettingsView.tsx +1535 -0
- package/src/client/sync/history-model.test.ts +194 -0
- package/src/client/sync/history-model.ts +195 -0
- package/src/client/sync/sync-api.test.ts +585 -0
- package/src/client/sync/sync-api.ts +543 -0
- package/src/client/sync/sync-locales.ts +416 -0
- package/src/client/sync/sync-push-preview.test.ts +52 -0
- package/src/client/sync/sync-view-v2.test.ts +213 -0
- package/src/client/sync/sync-view.test.ts +487 -0
- package/src/client/sync/sync-view.ts +759 -0
- package/src/core/analyzer.ts +1068 -0
- package/src/core/backup.ts +605 -0
- package/src/core/boot-rescue.test.ts +847 -0
- package/src/core/boot-rescue.ts +737 -0
- package/src/core/cache-cleaner.test.ts +169 -0
- package/src/core/cache-cleaner.ts +121 -0
- package/src/core/config-lifecycle.test.ts +727 -0
- package/src/core/config-lifecycle.ts +573 -0
- package/src/core/config-snapshot.test.ts +369 -0
- package/src/core/config-snapshot.ts +522 -0
- package/src/core/config-state.test.ts +381 -0
- package/src/core/config-state.ts +207 -0
- package/src/core/consult-source.test.ts +199 -0
- package/src/core/consult-source.ts +293 -0
- package/src/core/crash-report.test.ts +380 -0
- package/src/core/crash-report.ts +369 -0
- package/src/core/exporter.ts +417 -0
- package/src/core/ghost-sweep.test.ts +94 -0
- package/src/core/ghost-sweep.ts +67 -0
- package/src/core/importer.ts +83 -0
- package/src/core/index.ts +56 -0
- package/src/core/journal.test.ts +257 -0
- package/src/core/journal.ts +518 -0
- package/src/core/local-plugin-host.ts +152 -0
- package/src/core/local-plugin-pack.test.ts +359 -0
- package/src/core/local-plugin-pack.ts +363 -0
- package/src/core/messages.test.ts +54 -0
- package/src/core/messages.ts +664 -0
- package/src/core/migration-consult.test.ts +315 -0
- package/src/core/migration-consult.ts +494 -0
- package/src/core/migration-history.test.ts +319 -0
- package/src/core/migration-history.ts +484 -0
- package/src/core/model-tools.test.ts +190 -0
- package/src/core/model-tools.ts +208 -0
- package/src/core/msg-types.ts +10 -0
- package/src/core/phase3-child-crash.ts +95 -0
- package/src/core/phase3-host.ts +411 -0
- package/src/core/phase3-p1.test.ts +212 -0
- package/src/core/phase3-p2b-fingerprint.test.ts +189 -0
- package/src/core/phase3-prod-child.ts +36 -0
- package/src/core/phase3-production-integration.test.ts +286 -0
- package/src/core/phase4-crash-child.ts +101 -0
- package/src/core/phase4-crash-injection.test.ts +166 -0
- package/src/core/plugin-cli.fs.test.ts +184 -0
- package/src/core/plugin-cli.test.ts +137 -0
- package/src/core/plugin-cli.ts +563 -0
- package/src/core/reconcile.test.ts +392 -0
- package/src/core/reconcile.ts +498 -0
- package/src/core/recovery-orchestrator.test.ts +145 -0
- package/src/core/recovery-orchestrator.ts +366 -0
- package/src/core/restore-manage.test.ts +94 -0
- package/src/core/restore.ts +849 -0
- package/src/core/rollback.ts +151 -0
- package/src/core/run-progress.test.ts +237 -0
- package/src/core/run-registry.test.ts +232 -0
- package/src/core/run-registry.ts +209 -0
- package/src/core/smoke.test.ts +842 -0
- package/src/core/startup-barrier.ts +99 -0
- package/src/core/types.ts +542 -0
- package/src/core/undo.ts +116 -0
- package/src/core/validator.ts +84 -0
- package/src/core/verify-recovery.ts +265 -0
- package/src/core/watcher.test.ts +326 -0
- package/src/core/watcher.ts +263 -0
- package/src/index.facade.test.ts +265 -0
- package/src/index.sync.test.ts +217 -0
- package/src/index.ts +2975 -0
- package/src/migrations/index.ts +71 -0
- package/src/migrations/v1-to-v2.ts +17 -0
- package/src/profiles/index.ts +8 -0
- package/src/profiles/profile-manager.test.ts +282 -0
- package/src/profiles/profile-manager.ts +630 -0
- package/src/schema/config.ts +165 -0
- package/src/schema/index.ts +71 -0
- package/src/schema/manifest-schema.test.ts +519 -0
- package/src/schema/manifest.ts +124 -0
- package/src/schema/tombstones.test.ts +131 -0
- package/src/schema/tombstones.ts +89 -0
- package/src/schema/types.ts +245 -0
- package/src/schema/versions.ts +55 -0
- package/src/security/encryption.ts +335 -0
- package/src/security/index.ts +16 -0
- package/src/security/integrity.ts +102 -0
- package/src/security/redaction.ts +101 -0
- package/src/security/secret-scanner.ts +445 -0
- package/src/security/security.test.ts +1215 -0
- package/src/security/vault.test.ts +164 -0
- package/src/security/vault.ts +198 -0
- package/src/security/zip-security.ts +180 -0
- package/src/sync/autosync-config.test.ts +147 -0
- package/src/sync/autosync-config.ts +179 -0
- package/src/sync/autosync-scheduler.test.ts +490 -0
- package/src/sync/autosync-scheduler.ts +646 -0
- package/src/sync/fs.ts +48 -0
- package/src/sync/git/git-transport.test.ts +673 -0
- package/src/sync/git/git-transport.ts +491 -0
- package/src/sync/github-auth.test.ts +280 -0
- package/src/sync/github-auth.ts +356 -0
- package/src/sync/layout.test.ts +270 -0
- package/src/sync/layout.ts +222 -0
- package/src/sync/snapshot-json.test.ts +119 -0
- package/src/sync/snapshot-json.ts +128 -0
- package/src/sync/sync-config.test.ts +310 -0
- package/src/sync/sync-config.ts +299 -0
- package/src/sync/sync-engine.test.ts +1074 -0
- package/src/sync/sync-engine.ts +754 -0
- package/src/sync/sync-history.test.ts +116 -0
- package/src/sync/sync-history.ts +120 -0
- package/src/sync/sync-selection.test.ts +186 -0
- package/src/sync/sync-selection.ts +143 -0
- package/src/sync/sync-session.test.ts +143 -0
- package/src/sync/sync-session.ts +77 -0
- package/src/sync/sync-state.test.ts +164 -0
- package/src/sync/sync-state.ts +131 -0
- package/src/sync/transport.test.ts +111 -0
- package/src/sync/transport.ts +118 -0
- package/src/sync/ui-prefs.test.ts +152 -0
- package/src/sync/ui-prefs.ts +135 -0
- package/src/sync/webdav/webdav-transport.test.ts +871 -0
- package/src/sync/webdav/webdav-transport.ts +535 -0
- package/src/ui/errors.test.ts +47 -0
- package/src/ui/errors.ts +100 -0
- package/src/ui/export-flow.test.ts +87 -0
- package/src/ui/export-flow.ts +156 -0
- package/src/ui/i18n.ts +590 -0
- package/src/ui/migration-consult-view.test.ts +151 -0
- package/src/ui/migration-consult-view.ts +178 -0
- package/src/ui/progress.test.ts +47 -0
- package/src/ui/progress.ts +102 -0
- package/src/ui/report.test.ts +101 -0
- package/src/ui/report.ts +146 -0
- package/src/ui/test-helpers.ts +184 -0
- package/src/ui/types.ts +325 -0
- package/src/utils/atomic-write.test.ts +312 -0
- package/src/utils/atomic-write.ts +422 -0
- package/src/utils/env-lock.test.ts +999 -0
- package/src/utils/env-lock.ts +1086 -0
- package/src/utils/hashing.ts +53 -0
- package/src/utils/json.ts +77 -0
- package/src/utils/logger.ts +114 -0
- package/src/utils/paths.ts +176 -0
- package/src/utils/proxy.test.ts +465 -0
- package/src/utils/proxy.ts +493 -0
- package/src/utils/recursive-walk.test.ts +148 -0
- package/src/utils/recursive-walk.ts +145 -0
- package/src/utils/zip.ts +332 -0
package/src/core/undo.ts
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 撤销 / 重做语义(Phase 1 P0-2)。
|
|
3
|
+
*
|
|
4
|
+
* 与竞品 dsh-undo-savepoint 的语义对齐(其「撤销不会撤销掉自己」的等价物在本仓库
|
|
5
|
+
* 由两处保证:`pre-restore` 快照自身不参与候选,以及恢复后按内容指纹做回声抑制):
|
|
6
|
+
*
|
|
7
|
+
* 自动快照在变更**之后**采集,因此「最新快照」通常等于当前状态。
|
|
8
|
+
* 真实撤销 = 回退到与当前状态**内容不同**的最新快照(跳过等于当前状态的那些)。
|
|
9
|
+
* 全部相同 → 明确返回「没有可撤销的变化」,而不是做一次空操作。
|
|
10
|
+
*
|
|
11
|
+
* 重做的护栏:撤销时先落一个 `pre-restore` 快照记录「撤销前状态」;
|
|
12
|
+
* 一旦其后产生任何更新的快照(= 用户又改了),重做即失效——避免把用户的
|
|
13
|
+
* 新改动覆盖回旧状态。
|
|
14
|
+
*
|
|
15
|
+
* 本模块是**纯规划**:不读盘、不写盘、不碰 adapter。执行侧见 config-snapshot.ts。
|
|
16
|
+
*/
|
|
17
|
+
import { statesEqual, type ConfigState } from './config-state.ts';
|
|
18
|
+
|
|
19
|
+
/** 快照种类。pre-restore 为撤销/还原自身的记账快照,永不作为撤销目标。 */
|
|
20
|
+
export type ConfigSnapshotKind = 'manual' | 'auto' | 'undo' | 'pre-restore' | 'baseline';
|
|
21
|
+
|
|
22
|
+
/** 规划所需的最小快照视图(携带其采集到的状态) */
|
|
23
|
+
export interface UndoCandidate {
|
|
24
|
+
id: string;
|
|
25
|
+
createdAt: string;
|
|
26
|
+
kind: ConfigSnapshotKind;
|
|
27
|
+
/** 该快照采集时的配置状态(缺省 = 旧格式快照,无法参与内容比对) */
|
|
28
|
+
state?: ConfigState;
|
|
29
|
+
/** pre-restore 是否已被重做消费 */
|
|
30
|
+
consumed?: boolean;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export type UndoPlan =
|
|
34
|
+
| { kind: 'undo'; targetId: string; skippedNewer: number; reason: 'content-differs' }
|
|
35
|
+
| { kind: 'none'; reason: 'no-snapshots' | 'already-at-state' };
|
|
36
|
+
|
|
37
|
+
export type RedoPlan =
|
|
38
|
+
| { kind: 'redo'; targetId: string; reason: 'unconsumed-pre-restore' }
|
|
39
|
+
| { kind: 'none'; reason: 'no-pre-restore' | 'superseded-by-newer-change' };
|
|
40
|
+
|
|
41
|
+
/** 按 createdAt 倒序(新→旧);同刻按 id 稳定排序保证确定性。 */
|
|
42
|
+
export function sortNewestFirst<T extends { createdAt: string; id: string }>(items: readonly T[]): T[] {
|
|
43
|
+
return [...items].sort((a, b) => {
|
|
44
|
+
if (a.createdAt !== b.createdAt) return a.createdAt < b.createdAt ? 1 : -1;
|
|
45
|
+
return a.id < b.id ? 1 : a.id > b.id ? -1 : 0;
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** 可作为撤销目标/状态基准的快照(剔除 pre-restore 与无 state 的旧格式) */
|
|
50
|
+
function comparableCandidates(snapshots: readonly UndoCandidate[]): UndoCandidate[] {
|
|
51
|
+
return sortNewestFirst(snapshots).filter((s) => s.kind !== 'pre-restore' && s.state !== undefined);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* 规划一次撤销:返回第一个内容与 current 不同的快照。
|
|
56
|
+
* `skippedNewer` = 跳过的「与当前状态相同」的更新快照数(通常 ≥1,因为自动快照
|
|
57
|
+
* 落在变更之后)。
|
|
58
|
+
*/
|
|
59
|
+
export function planUndo(current: ConfigState, snapshots: readonly UndoCandidate[]): UndoPlan {
|
|
60
|
+
const candidates = comparableCandidates(snapshots);
|
|
61
|
+
if (candidates.length === 0) return { kind: 'none', reason: 'no-snapshots' };
|
|
62
|
+
for (let i = 0; i < candidates.length; i++) {
|
|
63
|
+
const candidate = candidates[i]!;
|
|
64
|
+
if (!statesEqual(current, candidate.state!)) {
|
|
65
|
+
return { kind: 'undo', targetId: candidate.id, skippedNewer: i, reason: 'content-differs' };
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return { kind: 'none', reason: 'already-at-state' };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** 是否存在可撤销的变化(供 UI 按钮可用性;等价于 planUndo 是否命中)。 */
|
|
72
|
+
export function canUndo(current: ConfigState, snapshots: readonly UndoCandidate[]): boolean {
|
|
73
|
+
return planUndo(current, snapshots).kind === 'undo';
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* 规划一次重做:取最新的未消费 pre-restore。
|
|
78
|
+
* 若其后存在任何更新的快照(含未消费的 pre-restore)→ superseded-by-newer-change。
|
|
79
|
+
*/
|
|
80
|
+
export function planRedo(snapshots: readonly UndoCandidate[]): RedoPlan {
|
|
81
|
+
const ordered = sortNewestFirst(snapshots);
|
|
82
|
+
const pre = ordered.find((s) => s.kind === 'pre-restore' && s.consumed !== true);
|
|
83
|
+
if (pre === undefined) return { kind: 'none', reason: 'no-pre-restore' };
|
|
84
|
+
const superseded = ordered.some(
|
|
85
|
+
(s) => s.createdAt > pre.createdAt && (s.kind !== 'pre-restore' || s.consumed !== true),
|
|
86
|
+
);
|
|
87
|
+
if (superseded) return { kind: 'none', reason: 'superseded-by-newer-change' };
|
|
88
|
+
return { kind: 'redo', targetId: pre.id, reason: 'unconsumed-pre-restore' };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** 是否存在可重做的撤销(供 UI 按钮可用性)。 */
|
|
92
|
+
export function canRedo(snapshots: readonly UndoCandidate[]): boolean {
|
|
93
|
+
return planRedo(snapshots).kind === 'redo';
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* 撤销后需要标记为「已被跨过」的快照 id 列表。
|
|
98
|
+
* 语义:撤销回退到 targetId 之后,比 targetId 更新、且内容等于**撤销前状态**的那些
|
|
99
|
+
* 快照,代表的是被撤销掉的那一步;重做回到撤销前状态后应清除该标记。
|
|
100
|
+
* 返回 targetId 之后(更旧侧不涉及)需要标记的 id。
|
|
101
|
+
*/
|
|
102
|
+
export function steppedIds(
|
|
103
|
+
snapshots: readonly UndoCandidate[],
|
|
104
|
+
targetId: string,
|
|
105
|
+
beforeState: ConfigState,
|
|
106
|
+
): string[] {
|
|
107
|
+
const target = snapshots.find((s) => s.id === targetId);
|
|
108
|
+
if (target === undefined) return [];
|
|
109
|
+
return snapshots
|
|
110
|
+
.filter((s) => s.kind !== 'pre-restore'
|
|
111
|
+
&& s.id !== targetId
|
|
112
|
+
&& s.createdAt > target.createdAt
|
|
113
|
+
&& s.state !== undefined
|
|
114
|
+
&& statesEqual(s.state, beforeState))
|
|
115
|
+
.map((s) => s.id);
|
|
116
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 整体校验与兼容性评分(规范 §18 完整性 / §30 兼容性评分 / 导入后校验)。
|
|
3
|
+
*/
|
|
4
|
+
import { validateManifest } from '../schema/manifest.ts';
|
|
5
|
+
import { validateSectionData } from '../schema/config.ts';
|
|
6
|
+
import { canImport, describeVersion } from '../schema/versions.ts';
|
|
7
|
+
import type { SectionId } from '../schema/types.ts';
|
|
8
|
+
import type { CompatibilityInput, CompatibilityScore, ValidationResult } from './types.ts';
|
|
9
|
+
|
|
10
|
+
export { validateManifest } from '../schema/manifest.ts';
|
|
11
|
+
export { validateSectionData } from '../schema/config.ts';
|
|
12
|
+
|
|
13
|
+
/** 校验 ZIP 内分区数据集合(空对象=合法);返回合并的校验结果 */
|
|
14
|
+
export function validateSections(
|
|
15
|
+
sections: ReadonlyMap<SectionId, unknown>,
|
|
16
|
+
): ValidationResult {
|
|
17
|
+
const issues: ValidationResult['issues'] = [];
|
|
18
|
+
for (const [sectionId, data] of sections) {
|
|
19
|
+
const sectionIssues = validateSectionData(sectionId, data);
|
|
20
|
+
for (const issue of sectionIssues) {
|
|
21
|
+
issues.push({ ...issue, path: `${sectionId}:${issue.path}` });
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
return { valid: issues.every((i) => i.severity !== 'error'), issues };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function parseVersion(v: string): { major: number; minor: number; patch: number; pre: number } {
|
|
28
|
+
const m = /^(\d+)\.(\d+)(?:\.(\d+))?(?:-(?:rc\.?)?(\d+))?/i.exec(v);
|
|
29
|
+
if (!m) return { major: 0, minor: 0, patch: 0, pre: 0 };
|
|
30
|
+
return { major: Number(m[1]), minor: Number(m[2]), patch: Number(m[3] ?? 0), pre: Number(m[4] ?? Infinity) };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** 版本比较:-1=a<b,0=相等,1=a>b(预发布 < 正式版) */
|
|
34
|
+
function compareVersions(a: ReturnType<typeof parseVersion>, b: ReturnType<typeof parseVersion>): -1 | 0 | 1 {
|
|
35
|
+
for (const k of ['major', 'minor', 'patch'] as const) {
|
|
36
|
+
if (a[k] !== b[k]) return a[k] < b[k] ? -1 : 1;
|
|
37
|
+
}
|
|
38
|
+
if (a.pre !== b.pre) return a.pre < b.pre ? -1 : 1;
|
|
39
|
+
return 0;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* 兼容性评分(规则驱动,不凭感觉):
|
|
44
|
+
* unsupported — schema 超出本插件支持范围(过高/过低)
|
|
45
|
+
* partial — 跨平台、分区缺失、或备份 DSH 比目标新
|
|
46
|
+
* good — 备份 DSH 比目标旧(向后兼容)且其余正常
|
|
47
|
+
* excellent — 同平台、无缺失、schema 支持
|
|
48
|
+
*/
|
|
49
|
+
export function computeCompatibility(input: CompatibilityInput): CompatibilityScore {
|
|
50
|
+
if (!canImport(input.schemaVersion)) return 'unsupported';
|
|
51
|
+
|
|
52
|
+
let score: CompatibilityScore = 'excellent';
|
|
53
|
+
|
|
54
|
+
if (input.sourcePlatform !== input.targetPlatform) score = 'partial';
|
|
55
|
+
if (input.missingSections.length > 0) score = 'partial';
|
|
56
|
+
|
|
57
|
+
const src = parseVersion(input.sourceDsh);
|
|
58
|
+
const tgt = parseVersion(input.targetDsh);
|
|
59
|
+
const cmp = compareVersions(src, tgt);
|
|
60
|
+
if (cmp > 0) {
|
|
61
|
+
// 备份来自更新的 DSH → 可能有目标版本不认识的配置
|
|
62
|
+
score = 'partial';
|
|
63
|
+
} else if (cmp < 0) {
|
|
64
|
+
// 备份来自更旧的 DSH → 目标向后兼容
|
|
65
|
+
score = 'good';
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return score;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** 兼容性得分的可读描述(UI 报告用) */
|
|
72
|
+
export function describeCompatibility(score: CompatibilityScore): string {
|
|
73
|
+
switch (score) {
|
|
74
|
+
case 'excellent': return 'Excellent — 完美兼容(同平台、无缺失、schema 支持)';
|
|
75
|
+
case 'good': return 'Good — 兼容(旧版备份导入新版 DSH)';
|
|
76
|
+
case 'partial': return 'Partial — 部分兼容(跨平台/分区缺失/版本超前,需人工确认)';
|
|
77
|
+
case 'unsupported': return 'Unsupported — 不受支持(schema 超出范围)';
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** 用 describeVersion 生成 schema 状态的用户可读说明 */
|
|
82
|
+
export function describeSchemaStatus(schemaVersion: number): string {
|
|
83
|
+
return describeVersion(schemaVersion);
|
|
84
|
+
}
|
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Phase 5 Post-Recovery Verification(§6)。
|
|
3
|
+
*
|
|
4
|
+
* 职责:recovery/rollback 完成后,验证目标状态与 trusted snapshot 匹配。
|
|
5
|
+
* 核心不变量(VERIFIED):**No recovery/rollback may be considered complete until the
|
|
6
|
+
* target state is verified to match the trusted snapshot** —— 验证失败不能 COMMITTED。
|
|
7
|
+
*
|
|
8
|
+
* 关键规则(§6.2 / §6.3 / §11.1):
|
|
9
|
+
* - **verify 时刻重验**:不信任 execute 时「已校验过」的结论(TOCTOU 窗口,快照可能被替换)。
|
|
10
|
+
* 本函数在 verify 时刻重跑完整 `validateSnapshotForRestore`(含 symlink + provenance +
|
|
11
|
+
* env binding + `requireOperationBound=true`),并从磁盘重读 snapshot.json 作为权威。
|
|
12
|
+
* - **只消费 TRUSTED_OPERATION_SNAPSHOT**:非 operation-bound(TRUSTED_MANUAL_LOCAL /
|
|
13
|
+
* LEGACY_REQUIRES_CONFIRMATION)一律拒绝,绝不当作 MATCH。
|
|
14
|
+
* - **verdict 语义严格区分**:
|
|
15
|
+
* MATCH 目标状态与 snapshot 完全匹配
|
|
16
|
+
* PARTIAL_MATCH 核心可验证状态匹配,但存在无法可靠验证的部分(如凭据值不可回读)
|
|
17
|
+
* MISMATCH 能确定目标状态仍不同于 snapshot(文件 hash 不匹配 / 应删仍存在 /
|
|
18
|
+
* plugin/settings 明确不同)
|
|
19
|
+
* VERIFICATION_ERROR 验证过程本身无法可靠完成(快照重验失败 / fs 读错误 / 异常)
|
|
20
|
+
* 绝不把 VERIFICATION_ERROR 降级成 PARTIAL_MATCH。
|
|
21
|
+
* - 输出结构兼容 Step 2 的 `recoveryVerification`(verdict/details[]/manualHints[]/at);
|
|
22
|
+
* details/manualHints 写入 journal 前由调用方过 `redactJournalText`(本函数只产出文本,
|
|
23
|
+
* 不含 secret 值——凭据值不可回读,settings 只比对不输出值)。
|
|
24
|
+
*
|
|
25
|
+
* 本模块为纯函数(fs 经 node:fs + HostContext 门面注入),可独立测试。
|
|
26
|
+
*/
|
|
27
|
+
import fs from 'node:fs/promises';
|
|
28
|
+
import path from 'node:path';
|
|
29
|
+
import { isDeepStrictEqual } from 'node:util';
|
|
30
|
+
|
|
31
|
+
import { resolveFileTarget } from './backup.ts';
|
|
32
|
+
import { validateSnapshotForRestore } from './restore.ts';
|
|
33
|
+
import type { RecoveryVerification, RecoveryVerificationVerdict } from './journal.ts';
|
|
34
|
+
import type { HostContext, Snapshot } from './types.ts';
|
|
35
|
+
import type { SectionId } from '../schema/types.ts';
|
|
36
|
+
|
|
37
|
+
export interface VerifyRecoveryOptions {
|
|
38
|
+
/** 快照根目录(<snapshotsDir>,<snapshotDir> 的直接父级)。 */
|
|
39
|
+
snapshotsRoot: string;
|
|
40
|
+
/** 当前环境指纹(必须与 snapshot binding 一致,否则 WRONG_ENVIRONMENT)。 */
|
|
41
|
+
environmentFingerprint: string;
|
|
42
|
+
/** 期望的 operationId(journal.operationId)。快照 operationId 必须与之双向一致,否则 WRONG_OPERATION。 */
|
|
43
|
+
expectedOperationId?: string;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** 越界防御:abs 必须等于 homeDir 或在 homeDir 之内 */
|
|
47
|
+
function isWithinHome(homeDir: string, abs: string): boolean {
|
|
48
|
+
const root = path.resolve(homeDir);
|
|
49
|
+
return abs === root || abs.startsWith(root + path.sep);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** 绝对路径 → 相对 homeDir(跨平台归一为 / 分隔;越界返回空串) */
|
|
53
|
+
function toRelPath(homeDir: string, abs: string): string {
|
|
54
|
+
if (!isWithinHome(homeDir, abs)) return '';
|
|
55
|
+
return path.relative(homeDir, abs).split(path.sep).join('/');
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** 快照目录内 blob 绝对路径(防 snapshot.json 里伪造 ../ 越界读) */
|
|
59
|
+
function blobAbs(snapshotDir: string, blobPath: string): string {
|
|
60
|
+
const abs = path.resolve(snapshotDir, blobPath);
|
|
61
|
+
if (!isWithinHome(snapshotDir, abs)) throw new Error(`blob 路径越界: ${blobPath}`);
|
|
62
|
+
return abs;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Uint8Array 字节级相等 */
|
|
66
|
+
function bufEq(a: Uint8Array, b: Uint8Array): boolean {
|
|
67
|
+
if (a.length !== b.length) return false;
|
|
68
|
+
for (let i = 0; i < a.length; i++) if (a[i] !== b[i]) return false;
|
|
69
|
+
return true;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** file 类条目的目标绝对路径(复用 backup.ts 的基准目录规则;仅读 homeDir) */
|
|
73
|
+
function resolveFileAbs(homeDir: string, adapter: SectionId, ref: string): string {
|
|
74
|
+
return resolveFileTarget({ homeDir } as unknown as HostContext, adapter, ref);
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Post-recovery verification(§6.4)。
|
|
78
|
+
*
|
|
79
|
+
* @param snapshot 被恢复 operation 的 journal 引用快照(id + binding 用于定位与交叉校验;
|
|
80
|
+
* 实际校验以磁盘重读为准,不信任内存对象)。
|
|
81
|
+
* @param ctx HostContext(homeDir / fs / settings / plugins 门面)。
|
|
82
|
+
* @param opts snapshotsRoot + environmentFingerprint。
|
|
83
|
+
*/
|
|
84
|
+
export async function verifyRecovery(
|
|
85
|
+
snapshot: Snapshot,
|
|
86
|
+
ctx: HostContext,
|
|
87
|
+
opts: VerifyRecoveryOptions,
|
|
88
|
+
): Promise<RecoveryVerification> {
|
|
89
|
+
const details: string[] = [];
|
|
90
|
+
const manualHints: string[] = [];
|
|
91
|
+
const at = new Date().toISOString();
|
|
92
|
+
const snapshotDir = path.join(opts.snapshotsRoot, snapshot.id);
|
|
93
|
+
|
|
94
|
+
// 1) verify 时刻重验快照(TOCTOU):重跑完整 validateSnapshotForRestore,requireOperationBound=true。
|
|
95
|
+
// 只接受 TRUSTED_OPERATION_SNAPSHOT;任何其它 verdict(含 TRUSTED_MANUAL_LOCAL / LEGACY)→ VERIFICATION_ERROR。
|
|
96
|
+
const val = await validateSnapshotForRestore(snapshotDir, opts.snapshotsRoot, {
|
|
97
|
+
environmentFingerprint: opts.environmentFingerprint,
|
|
98
|
+
});
|
|
99
|
+
if (val.verdict !== 'TRUSTED_OPERATION_SNAPSHOT') {
|
|
100
|
+
return {
|
|
101
|
+
verdict: 'VERIFICATION_ERROR',
|
|
102
|
+
details: [`快照重验失败: ${val.verdict}${val.reason ? ` — ${val.reason}` : ''}`],
|
|
103
|
+
manualHints: [],
|
|
104
|
+
at,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
details.push('快照重验通过(TRUSTED_OPERATION_SNAPSHOT)');
|
|
108
|
+
|
|
109
|
+
// 2) 从磁盘重读 snapshot.json 作为权威(不信任传入的 snapshot 对象,防 execute→verify 间替换)。
|
|
110
|
+
let diskSnapshot: Snapshot;
|
|
111
|
+
try {
|
|
112
|
+
const raw = await fs.readFile(path.join(snapshotDir, 'snapshot.json'), 'utf8');
|
|
113
|
+
diskSnapshot = JSON.parse(raw) as Snapshot;
|
|
114
|
+
} catch (err) {
|
|
115
|
+
return { verdict: 'VERIFICATION_ERROR', details: [...details, `snapshot.json 重读失败: ${err instanceof Error ? err.message : String(err)}`], manualHints: [], at };
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// 3) environment binding(validateSnapshotForRestore 已查,此处双保险)
|
|
119
|
+
if (diskSnapshot.environmentFingerprint && diskSnapshot.environmentFingerprint !== opts.environmentFingerprint) {
|
|
120
|
+
return { verdict: 'VERIFICATION_ERROR', details: [...details, 'environmentFingerprint 不匹配(WRONG_ENVIRONMENT)'], manualHints: [], at };
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// 3b) operation binding(journal.operationId ↔ snapshot.operationId 双向一致;WRONG_OPERATION 拒绝)
|
|
124
|
+
if (opts.expectedOperationId !== undefined && diskSnapshot.operationId !== opts.expectedOperationId) {
|
|
125
|
+
return { verdict: 'VERIFICATION_ERROR', details: [...details, `operationId 不匹配(WRONG_OPERATION): snapshot=${diskSnapshot.operationId ?? '(无)'} expected=${opts.expectedOperationId}`], manualHints: [], at };
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
let mismatch = false;
|
|
129
|
+
let unverifiable = false;
|
|
130
|
+
|
|
131
|
+
// 4) host files(hostFileBackups):existed=true 应存在且内容 hash 匹配 blob;existed=false 应不存在。
|
|
132
|
+
// 经 ctx.fs(HostContext 注入,homeDir 相对)读写,与 rollback.ts 一致。
|
|
133
|
+
for (const backup of diskSnapshot.hostFileBackups ?? []) {
|
|
134
|
+
const rel = backup.relPath;
|
|
135
|
+
if (backup.existed) {
|
|
136
|
+
if (backup.blobPath === '') {
|
|
137
|
+
unverifiable = true;
|
|
138
|
+
details.push(`host file 无 blob 信息: ${rel}`);
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
if (!(await ctx.fs.exists(rel))) {
|
|
142
|
+
mismatch = true;
|
|
143
|
+
details.push(`host file 缺失: ${rel}`);
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
let blob: Uint8Array;
|
|
147
|
+
try {
|
|
148
|
+
blob = await fs.readFile(blobAbs(snapshotDir, backup.blobPath));
|
|
149
|
+
} catch {
|
|
150
|
+
unverifiable = true;
|
|
151
|
+
details.push(`host file blob 读取失败: ${rel}`);
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
const current = await ctx.fs.readFile(rel);
|
|
155
|
+
if (!bufEq(current, blob)) {
|
|
156
|
+
mismatch = true;
|
|
157
|
+
details.push(`host file 内容不匹配: ${rel}`);
|
|
158
|
+
} else {
|
|
159
|
+
details.push(`host file 匹配: ${rel}`);
|
|
160
|
+
}
|
|
161
|
+
} else {
|
|
162
|
+
// snapshot expects absent → target 必须不存在(删除动作失败 → 残留被检测)
|
|
163
|
+
if (await ctx.fs.exists(rel)) {
|
|
164
|
+
mismatch = true;
|
|
165
|
+
details.push(`应删除目标仍存在: ${rel}`);
|
|
166
|
+
} else {
|
|
167
|
+
details.push(`删除目标已不存在: ${rel}`);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// 5) file 类条目(entries kind='file'):existed=true 应存在且内容匹配 blob;existed=false 应不存在。
|
|
173
|
+
for (const entry of diskSnapshot.entries ?? []) {
|
|
174
|
+
if (entry.kind !== 'file') continue;
|
|
175
|
+
const rel = toRelPath(ctx.homeDir, resolveFileAbs(ctx.homeDir, entry.adapter, entry.ref));
|
|
176
|
+
if (rel === '') {
|
|
177
|
+
unverifiable = true;
|
|
178
|
+
details.push(`file 路径越界: ${entry.adapter}:${entry.ref}`);
|
|
179
|
+
continue;
|
|
180
|
+
}
|
|
181
|
+
if (entry.existed && entry.copiedTo) {
|
|
182
|
+
if (!(await ctx.fs.exists(rel))) {
|
|
183
|
+
mismatch = true;
|
|
184
|
+
details.push(`file 缺失: ${entry.adapter}:${entry.ref}`);
|
|
185
|
+
continue;
|
|
186
|
+
}
|
|
187
|
+
let blob: Uint8Array;
|
|
188
|
+
try {
|
|
189
|
+
blob = await fs.readFile(blobAbs(snapshotDir, entry.copiedTo));
|
|
190
|
+
} catch {
|
|
191
|
+
unverifiable = true;
|
|
192
|
+
details.push(`file blob 读取失败: ${entry.adapter}:${entry.ref}`);
|
|
193
|
+
continue;
|
|
194
|
+
}
|
|
195
|
+
const current = await ctx.fs.readFile(rel);
|
|
196
|
+
if (!bufEq(current, blob)) {
|
|
197
|
+
mismatch = true;
|
|
198
|
+
details.push(`file 内容不匹配: ${entry.adapter}:${entry.ref}`);
|
|
199
|
+
} else {
|
|
200
|
+
details.push(`file 匹配: ${entry.adapter}:${entry.ref}`);
|
|
201
|
+
}
|
|
202
|
+
} else if (!entry.existed) {
|
|
203
|
+
if (await ctx.fs.exists(rel)) {
|
|
204
|
+
mismatch = true;
|
|
205
|
+
details.push(`应删除 file 目标仍存在: ${entry.adapter}:${entry.ref}`);
|
|
206
|
+
} else {
|
|
207
|
+
details.push(`删除 file 目标已不存在: ${entry.adapter}:${entry.ref}`);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// 6) plugin state(best-effort):导入期间新增插件应被移除。可精确验证 → 纳入 MATCH 判断;
|
|
213
|
+
// 无法可靠验证(无基线 / listInstalled 失败)→ PARTIAL_MATCH + manualHint。
|
|
214
|
+
const beforePlugins = diskSnapshot.beforePlugins;
|
|
215
|
+
if (beforePlugins === undefined) {
|
|
216
|
+
unverifiable = true;
|
|
217
|
+
details.push('快照无 beforePlugins 基线,插件状态不可验证');
|
|
218
|
+
manualHints.push('插件状态无法自动验证(快照无基线),请人工确认已装插件');
|
|
219
|
+
} else {
|
|
220
|
+
try {
|
|
221
|
+
const current = await ctx.plugins.listInstalled();
|
|
222
|
+
const beforeNames = new Set(beforePlugins.map((p) => p.name));
|
|
223
|
+
const added = current.filter((p) => !beforeNames.has(p.name));
|
|
224
|
+
if (added.length > 0) {
|
|
225
|
+
mismatch = true;
|
|
226
|
+
details.push(`导入期间新增插件未移除: ${added.map((p) => p.name).join(', ')}`);
|
|
227
|
+
} else {
|
|
228
|
+
details.push('插件状态匹配(无新增插件)');
|
|
229
|
+
}
|
|
230
|
+
} catch {
|
|
231
|
+
unverifiable = true;
|
|
232
|
+
details.push('插件状态无法验证(listInstalled 失败)');
|
|
233
|
+
manualHints.push('插件状态无法自动验证(listInstalled 失败),请人工确认已装插件');
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// 7) settings(best-effort):settingsNamespace 条目 before 值应与当前 settings 一致。
|
|
238
|
+
const settingsEntries = (diskSnapshot.entries ?? []).filter((e) => e.kind === 'settingsNamespace');
|
|
239
|
+
for (const entry of settingsEntries) {
|
|
240
|
+
try {
|
|
241
|
+
const current = await ctx.settings.describe(entry.ref);
|
|
242
|
+
if (!isDeepStrictEqual(current.value, entry.before)) {
|
|
243
|
+
mismatch = true;
|
|
244
|
+
details.push(`settings 不匹配: ${entry.ref}`);
|
|
245
|
+
} else {
|
|
246
|
+
details.push(`settings 匹配: ${entry.ref}`);
|
|
247
|
+
}
|
|
248
|
+
} catch {
|
|
249
|
+
unverifiable = true;
|
|
250
|
+
details.push(`settings 无法验证: ${entry.ref}`);
|
|
251
|
+
manualHints.push(`settings 无法自动验证(${entry.ref}),请人工确认`);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// 8) verdict:MISMATCH 优先(能确定目标状态不同);其次 PARTIAL_MATCH(存在不可验证项);否则 MATCH。
|
|
256
|
+
if (mismatch) return { verdict: 'MISMATCH', details, manualHints, at };
|
|
257
|
+
if (unverifiable) return { verdict: 'PARTIAL_MATCH', details, manualHints, at };
|
|
258
|
+
return { verdict: 'MATCH', details, manualHints, at };
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/** 便捷:把 RecoveryVerification 映射为 journal 终态(供 verify 路由单次原子 update 使用)。 */
|
|
262
|
+
export function recoveryTerminalState(verdict: RecoveryVerificationVerdict): 'ROLLED_BACK' | 'RECOVERED' | 'NEEDS_ATTENTION' {
|
|
263
|
+
if (verdict === 'MATCH' || verdict === 'PARTIAL_MATCH') return 'ROLLED_BACK';
|
|
264
|
+
return 'NEEDS_ATTENTION';
|
|
265
|
+
}
|