@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
|
@@ -0,0 +1,389 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reconciler(Phase 3:startup reconciliation)。
|
|
3
|
+
*
|
|
4
|
+
* 职责:扫描 active journals → parse/schema 校验(corrupt 隔离)→ 验 environment fingerprint
|
|
5
|
+
* → 验 Journal→Lock 绑定 → 验快照 → 验 step 指纹 → 外部探测 → 选择推荐 recovery outcome
|
|
6
|
+
* → 执行(需用户确认的破坏性动作才执行)→ recovery-history → 幂等。
|
|
7
|
+
*
|
|
8
|
+
* **Reconciler 不拥有 Environment Lock primitive 实现** —— 通过 Coordinator/port 使用
|
|
9
|
+
* (release、active≤1、recover 显式流程由宿主/Coordinator 编排)。
|
|
10
|
+
*
|
|
11
|
+
* **Startup(Rev 3 P1-NEW-2)不自动 recover stale lock**:
|
|
12
|
+
* - 若发现 stale lock + incomplete journal → RECOVERY_REQUIRED / SAFE MODE,不自动 recoverStaleLock;
|
|
13
|
+
* - 用户显式确认后才 prove stale → recover → acquire → reconcile。
|
|
14
|
+
*/
|
|
15
|
+
import { JournalStore, isTerminalState, transitionJournalState, JOURNAL_SCHEMA_VERSION, } from './journal.js';
|
|
16
|
+
// ---------- Reconcile ----------
|
|
17
|
+
/**
|
|
18
|
+
* 分析 active/ 下的所有 journal,返回每个 op 的决策 + 是否需 SAFE MODE。
|
|
19
|
+
* 只读(除 corrupt quarantine 与 terminal organize / recovery-history 外,不做 data mutation)。
|
|
20
|
+
*/
|
|
21
|
+
export async function reconcileActive(store, hooks, env, opts = {}) {
|
|
22
|
+
const decisions = [];
|
|
23
|
+
const unresolved = [];
|
|
24
|
+
let safeModeRequired = false;
|
|
25
|
+
const activeIds = await store.scanActive();
|
|
26
|
+
for (const operationId of activeIds) {
|
|
27
|
+
const outcome = await reconcileOne(store, hooks, env, opts, operationId);
|
|
28
|
+
decisions.push(outcome.decision);
|
|
29
|
+
if (outcome.safeMode)
|
|
30
|
+
safeModeRequired = true;
|
|
31
|
+
if (outcome.unresolved)
|
|
32
|
+
unresolved.push(operationId);
|
|
33
|
+
}
|
|
34
|
+
return { decisions, safeModeRequired, unresolved };
|
|
35
|
+
}
|
|
36
|
+
async function reconcileOne(store, hooks, env, opts, operationId) {
|
|
37
|
+
const j = await store.loadActive(operationId);
|
|
38
|
+
if (j === null) {
|
|
39
|
+
// 存在但 parse/校验失败 → corrupt → quarantine
|
|
40
|
+
const reason = `journal ${operationId} 无法解析或 schema 非法(隔离处理)`;
|
|
41
|
+
if (opts.autoQuarantine !== false) {
|
|
42
|
+
try {
|
|
43
|
+
await store.quarantine(operationId, reason);
|
|
44
|
+
}
|
|
45
|
+
catch { /* 隔离失败由上层诊断 */ }
|
|
46
|
+
}
|
|
47
|
+
await store.writeSafeMode(true).catch(() => undefined);
|
|
48
|
+
return { decision: { operationId, kind: 'corrupt', reason, snapshotId: null }, safeMode: true, unresolved: true };
|
|
49
|
+
}
|
|
50
|
+
// live owner 检查
|
|
51
|
+
if (await env.isLiveOwner(j)) {
|
|
52
|
+
return { decision: { operationId, kind: 'live', reason: 'owner 存活,live operation 进行中', snapshotId: j.snapshotId }, safeMode: false, unresolved: false };
|
|
53
|
+
}
|
|
54
|
+
// P1-A:ownership binding 强制校验(显式 recovery 捕获的 crashed ownership 证据)。
|
|
55
|
+
// journal.ownerInstanceId/lockId 必须匹配期望的 crashed ownership instanceId,否则不作 trusted 恢复。
|
|
56
|
+
if (env.expectedOwnershipInstanceId !== undefined && env.expectedOwnershipInstanceId !== '') {
|
|
57
|
+
const bindingOk = j.ownerInstanceId === env.expectedOwnershipInstanceId
|
|
58
|
+
&& j.lockId === env.expectedOwnershipInstanceId; // lockId = 同 epoch identity(ownerInstanceId)
|
|
59
|
+
if (!bindingOk) {
|
|
60
|
+
const reason = `ownership binding 不匹配(journal.ownerInstanceId/lockId 非 crashed ownership ${env.expectedOwnershipInstanceId}),不作 trusted 恢复`;
|
|
61
|
+
await store.writeSafeMode(true).catch(() => undefined);
|
|
62
|
+
await store.appendRecoveryHistory('binding-mismatch', { operationId, at: new Date().toISOString(), reason }).catch(() => undefined);
|
|
63
|
+
return { decision: { operationId, kind: 'needs-attention', reason, snapshotId: j.snapshotId }, safeMode: true, unresolved: true };
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
// env fingerprint
|
|
67
|
+
if (env.environmentFingerprint && j.environmentFingerprint && env.environmentFingerprint !== j.environmentFingerprint) {
|
|
68
|
+
const reason = `environment fingerprint 不匹配(跨机/环境变化),不自动恢复`;
|
|
69
|
+
await store.writeSafeMode(true).catch(() => undefined);
|
|
70
|
+
return { decision: { operationId, kind: 'env-mismatch', reason, snapshotId: j.snapshotId }, safeMode: true, unresolved: true };
|
|
71
|
+
}
|
|
72
|
+
// terminal 遗留 active → 规整到 completed(无实质 recovery)
|
|
73
|
+
if (isTerminalState(j.state)) {
|
|
74
|
+
if (j.state === 'NEEDS_ATTENTION') {
|
|
75
|
+
// NEEDS_ATTENTION 是「待用户确认」状态,保持 active 可见(不静默规整),SAFE MODE
|
|
76
|
+
await store.writeSafeMode(true).catch(() => undefined);
|
|
77
|
+
return { decision: { operationId, kind: 'needs-attention', reason: 'NEEDS_ATTENTION 待用户确认', snapshotId: j.snapshotId }, safeMode: true, unresolved: true };
|
|
78
|
+
}
|
|
79
|
+
// COMMITTED / ROLLED_BACK / RECOVERED 遗留 → 规整
|
|
80
|
+
if (opts.organizeTerminal !== false) {
|
|
81
|
+
try {
|
|
82
|
+
await store.moveToCompleted(operationId);
|
|
83
|
+
}
|
|
84
|
+
catch { /* move 失败不影响 */ }
|
|
85
|
+
}
|
|
86
|
+
return { decision: { operationId, kind: 'organized', reason: `terminal(${j.state}) journal 规整到 completed`, snapshotId: j.snapshotId }, safeMode: false, unresolved: false };
|
|
87
|
+
}
|
|
88
|
+
// 回滚进行中(ROLLING_BACK)或已部分补偿(entryDone 非空):**绝不判 RECOVERED**
|
|
89
|
+
// (P0-B:崩溃在回滚中 = 半回滚态,判 recovered 会把破坏状态误报为成功)。
|
|
90
|
+
if (j.state === 'ROLLING_BACK' || Object.keys(j.rollback.entryDone ?? {}).length > 0) {
|
|
91
|
+
const reason = `回滚中断(${j.state === 'ROLLING_BACK' ? 'ROLLING_BACK' : `entryDone ${Object.keys(j.rollback.entryDone).join(',')}`}),需显式续跑回滚(用户确认)`;
|
|
92
|
+
await store.writeSafeMode(true).catch(() => undefined);
|
|
93
|
+
await store.appendRecoveryHistory('rollback-interrupted', { operationId, at: new Date().toISOString(), reason }).catch(() => undefined);
|
|
94
|
+
return { decision: { operationId, kind: 'rollback-continue', reason, snapshotId: j.snapshotId }, safeMode: true, unresolved: true };
|
|
95
|
+
}
|
|
96
|
+
// §6.5 recovery hard gate(Reviewer A P0 + 第三轮 P2 收紧):
|
|
97
|
+
// 置于 entryDone/回滚中断检查(:180)之后、step 指纹判定(:187)之前。
|
|
98
|
+
// RECOVERING +(无 recoveryVerification 或 verdict 非 MATCH/PARTIAL_MATCH)→ needs-attention,
|
|
99
|
+
// **绝不自动判 RECOVERED**(VERIFIED 不变量:恢复未验证不得视为完成)。
|
|
100
|
+
// 只有 recoveryVerification.verdict === 'MATCH' | 'PARTIAL_MATCH' 的 RECOVERING journal
|
|
101
|
+
// 才可转 ROLLED_BACK / RECOVERED(该终态迁移由 verify 路由以单次原子 journal update 完成)。
|
|
102
|
+
if (j.state === 'RECOVERING') {
|
|
103
|
+
const v = j.recoveryVerification;
|
|
104
|
+
const verified = v !== undefined && (v.verdict === 'MATCH' || v.verdict === 'PARTIAL_MATCH');
|
|
105
|
+
if (!verified) {
|
|
106
|
+
const reason = 'RECOVERING 且 recovery 未验证(无 recoveryVerification 或 verdict 非 MATCH/PARTIAL_MATCH),绝不自动 RECOVERED';
|
|
107
|
+
await store.writeSafeMode(true).catch(() => undefined);
|
|
108
|
+
await store.update(operationId, (cur) => {
|
|
109
|
+
let next = transitionJournalState(cur, 'NEEDS_ATTENTION');
|
|
110
|
+
next = { ...next, error: next.error || reason, recovery: { ...next.recovery, reason, attempts: next.recovery.attempts + 1 } };
|
|
111
|
+
return next;
|
|
112
|
+
}).catch(() => undefined);
|
|
113
|
+
await store.appendRecoveryHistory('recovery-unverified', { operationId, at: new Date().toISOString(), reason }).catch(() => undefined);
|
|
114
|
+
return { decision: { operationId, kind: 'needs-attention', reason, snapshotId: j.snapshotId }, safeMode: true, unresolved: true };
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
// incomplete:判定 steps
|
|
118
|
+
const stepIds = [...new Set([...j.plannedSteps, ...Object.keys(j.steps)])];
|
|
119
|
+
let anyApplied = false;
|
|
120
|
+
let allDoneConfirmed = true;
|
|
121
|
+
let anyUnprovable = false;
|
|
122
|
+
let anyExternalUnknown = false;
|
|
123
|
+
const details = [];
|
|
124
|
+
for (const stepId of stepIds) {
|
|
125
|
+
const step = j.steps[stepId];
|
|
126
|
+
const plannedExternal = step?.external === true;
|
|
127
|
+
if (step === undefined) {
|
|
128
|
+
// Review D P2:plannedSteps 引用悬空(steps 缺条目)→ 不可证明,绝不能经 noop 判 RECOVERED。
|
|
129
|
+
anyUnprovable = true;
|
|
130
|
+
allDoneConfirmed = false;
|
|
131
|
+
details.push(`planned step ${stepId} 缺少 steps 条目(损坏/篡改 journal)`);
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
if (plannedExternal) {
|
|
135
|
+
// 外部 step:probe 判定
|
|
136
|
+
const probe = await hooks.probeExternal(step);
|
|
137
|
+
if (probe === 'installed') {
|
|
138
|
+
anyApplied = true;
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
if (probe === 'not-installed') {
|
|
142
|
+
continue;
|
|
143
|
+
} // 未装,无副作用
|
|
144
|
+
// half-installed / reverse-residue / unknown → 不可证明
|
|
145
|
+
anyExternalUnknown = true;
|
|
146
|
+
anyUnprovable = true;
|
|
147
|
+
allDoneConfirmed = false;
|
|
148
|
+
details.push(`external ${step.kind} ${step.ref}: ${probe}`);
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
// 本地 step:指纹判定
|
|
152
|
+
if (step.status === 'done') {
|
|
153
|
+
// 即使 done 也重验 afterFp(Windows rename 顺序失效)
|
|
154
|
+
let verdict;
|
|
155
|
+
try {
|
|
156
|
+
verdict = await hooks.verifyStepFingerprint(step);
|
|
157
|
+
}
|
|
158
|
+
catch {
|
|
159
|
+
verdict = 'unable';
|
|
160
|
+
}
|
|
161
|
+
if (verdict === 'after-match') {
|
|
162
|
+
anyApplied = true;
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
if (verdict === 'before-match') {
|
|
166
|
+
anyApplied = false;
|
|
167
|
+
details.push(`step ${step.ref} 未可靠完成(磁盘==before)`);
|
|
168
|
+
allDoneConfirmed = false;
|
|
169
|
+
anyUnprovable = true;
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
172
|
+
anyUnprovable = true;
|
|
173
|
+
allDoneConfirmed = false;
|
|
174
|
+
details.push(`step ${step.ref} 指纹 ${verdict}`);
|
|
175
|
+
}
|
|
176
|
+
else {
|
|
177
|
+
// planned/attention/failed → 未完成
|
|
178
|
+
allDoneConfirmed = false;
|
|
179
|
+
if (step.status === 'attention' || step.status === 'failed') {
|
|
180
|
+
anyUnprovable = true;
|
|
181
|
+
details.push(`step ${step.ref} ${step.status}`);
|
|
182
|
+
}
|
|
183
|
+
else { // planned:指纹判定 side effect 是否发生
|
|
184
|
+
let verdict;
|
|
185
|
+
try {
|
|
186
|
+
verdict = await hooks.verifyStepFingerprint(step);
|
|
187
|
+
}
|
|
188
|
+
catch {
|
|
189
|
+
verdict = 'unable';
|
|
190
|
+
}
|
|
191
|
+
if (verdict === 'after-match') {
|
|
192
|
+
anyApplied = true;
|
|
193
|
+
}
|
|
194
|
+
else if (verdict === 'before-match') { /* 未应用,安全 */ }
|
|
195
|
+
else {
|
|
196
|
+
anyUnprovable = true;
|
|
197
|
+
anyApplied = anyApplied || verdict === 'none';
|
|
198
|
+
details.push(`step ${step.ref} ${verdict}`);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
const snapshotExists = await hooks.snapshotExists(j.snapshotId, {
|
|
204
|
+
operationId: j.operationId,
|
|
205
|
+
ownerInstanceId: j.ownerInstanceId,
|
|
206
|
+
environmentFingerprint: j.environmentFingerprint,
|
|
207
|
+
});
|
|
208
|
+
// F20 修复:空 steps(opaque intent journal)不得因 [].every() 真空真而判 RECOVERED。
|
|
209
|
+
// 若 mutation 是否执行无法证明 → NEEDS_ATTENTION;若有 trusted bound snapshot → rollback-recommended。
|
|
210
|
+
if (stepIds.length === 0) {
|
|
211
|
+
// CREATED / SNAPSHOT_CREATED:mutation 尚未开始 → 安全 no-op
|
|
212
|
+
if (j.state === 'CREATED' || j.state === 'SNAPSHOT_CREATED') {
|
|
213
|
+
await store.update(operationId, (cur) => {
|
|
214
|
+
let next = transitionJournalState(cur, 'RECOVERED');
|
|
215
|
+
next = { ...next, recovery: { ...next.recovery, attemptedAt: new Date().toISOString(), outcome: 'RECOVERED', reason: 'no mutation started (empty steps, pre-APPLYING)', attempts: next.recovery.attempts + 1 } };
|
|
216
|
+
return next;
|
|
217
|
+
});
|
|
218
|
+
await store.moveToCompleted(operationId).catch(() => undefined);
|
|
219
|
+
await store.appendRecoveryHistory('noop', { operationId, at: new Date().toISOString(), reason: 'no mutation started' }).catch(() => undefined);
|
|
220
|
+
return { decision: { operationId, kind: 'noop', reason: 'mutation 未开始,安全完成', snapshotId: j.snapshotId }, safeMode: false, unresolved: false };
|
|
221
|
+
}
|
|
222
|
+
// APPLYING(或其它非 terminal):mutation 可能已开始,无法证明
|
|
223
|
+
if (j.snapshotId !== null && snapshotExists) {
|
|
224
|
+
// 有 trusted bound snapshot → rollback-recommended(需用户确认)
|
|
225
|
+
await store.writeSafeMode(true).catch(() => undefined);
|
|
226
|
+
const reason = 'opaque APPLYING(空 steps)且 mutation 可能已开始,有 trusted snapshot 可回滚(需用户确认)';
|
|
227
|
+
await store.update(operationId, (cur) => {
|
|
228
|
+
let next = transitionJournalState(cur, 'NEEDS_ATTENTION');
|
|
229
|
+
next = { ...next, error: next.error || reason, recovery: { ...next.recovery, reason, attempts: next.recovery.attempts + 1 } };
|
|
230
|
+
return next;
|
|
231
|
+
}).catch(() => undefined);
|
|
232
|
+
await store.appendRecoveryHistory('needs-attention', { operationId, at: new Date().toISOString(), reason }).catch(() => undefined);
|
|
233
|
+
return { decision: { operationId, kind: 'rollback-recommended', reason, snapshotId: j.snapshotId }, safeMode: true, unresolved: true };
|
|
234
|
+
}
|
|
235
|
+
// 无 trusted snapshot → NEEDS_ATTENTION(SAFE MODE)
|
|
236
|
+
await store.writeSafeMode(true).catch(() => undefined);
|
|
237
|
+
const reason = 'opaque APPLYING(空 steps)且 mutation 可能已开始,无 trusted snapshot 可回滚';
|
|
238
|
+
await store.update(operationId, (cur) => {
|
|
239
|
+
let next = transitionJournalState(cur, 'NEEDS_ATTENTION');
|
|
240
|
+
next = { ...next, error: next.error || reason, recovery: { ...next.recovery, reason, attempts: next.recovery.attempts + 1 } };
|
|
241
|
+
return next;
|
|
242
|
+
}).catch(() => undefined);
|
|
243
|
+
await store.appendRecoveryHistory('needs-attention', { operationId, at: new Date().toISOString(), reason }).catch(() => undefined);
|
|
244
|
+
return { decision: { operationId, kind: 'needs-attention', reason, snapshotId: j.snapshotId }, safeMode: true, unresolved: true };
|
|
245
|
+
}
|
|
246
|
+
// 决策
|
|
247
|
+
if (allDoneConfirmed && !anyUnprovable) {
|
|
248
|
+
// 所有 step 已 prove done → resume(RECOVERED),不重做
|
|
249
|
+
await store.update(operationId, (cur) => {
|
|
250
|
+
let next = transitionJournalState(cur, 'RECOVERED');
|
|
251
|
+
next = { ...next, recovery: { ...next.recovery, attemptedAt: new Date().toISOString(), outcome: 'RECOVERED', reason: 'all steps confirmed done', attempts: next.recovery.attempts + 1 } };
|
|
252
|
+
return next;
|
|
253
|
+
});
|
|
254
|
+
await store.moveToCompleted(operationId).catch(() => undefined);
|
|
255
|
+
await store.appendRecoveryHistory('recovered', { operationId, at: new Date().toISOString(), reason: 'all steps proven done' }).catch(() => undefined);
|
|
256
|
+
return { decision: { operationId, kind: 'recovered', reason: 'all steps proven applied', snapshotId: j.snapshotId }, safeMode: false, unresolved: false };
|
|
257
|
+
}
|
|
258
|
+
if (!anyApplied && !anyUnprovable) {
|
|
259
|
+
// 无任何 step 应用(全 beforeFp / all planned)+ 无外部/不可指纹 → no-op 安全丢弃
|
|
260
|
+
await store.update(operationId, (cur) => {
|
|
261
|
+
let next = transitionJournalState(cur, 'RECOVERED');
|
|
262
|
+
next = { ...next, recovery: { ...next.recovery, attemptedAt: new Date().toISOString(), outcome: 'RECOVERED', reason: 'no step applied', attempts: next.recovery.attempts + 1 } };
|
|
263
|
+
return next;
|
|
264
|
+
});
|
|
265
|
+
await store.moveToCompleted(operationId).catch(() => undefined);
|
|
266
|
+
await store.appendRecoveryHistory('noop', { operationId, at: new Date().toISOString(), reason: 'no step applied' }).catch(() => undefined);
|
|
267
|
+
return { decision: { operationId, kind: 'noop', reason: '无副作用可恢复,安全完成', snapshotId: j.snapshotId }, safeMode: false, unresolved: false };
|
|
268
|
+
}
|
|
269
|
+
if (anyUnprovable || anyExternalUnknown || !snapshotExists) {
|
|
270
|
+
// 不可证明 / 外部未知 / 快照缺失 → NEEDS_ATTENTION(SAFE MODE)
|
|
271
|
+
await store.writeSafeMode(true).catch(() => undefined);
|
|
272
|
+
const reason = `不可证明步骤: ${details.join('; ') || '外部副作用未知'}` + (snapshotExists ? '' : ';快照缺失');
|
|
273
|
+
await store.update(operationId, (cur) => {
|
|
274
|
+
let next = transitionJournalState(cur, 'NEEDS_ATTENTION');
|
|
275
|
+
next = { ...next, error: next.error || reason, recovery: { ...next.recovery, reason, attempts: next.recovery.attempts + 1 } };
|
|
276
|
+
return next;
|
|
277
|
+
}).catch(() => undefined);
|
|
278
|
+
await store.appendRecoveryHistory('needs-attention', { operationId, at: new Date().toISOString(), reason }).catch(() => undefined);
|
|
279
|
+
return { decision: { operationId, kind: 'needs-attention', reason, snapshotId: j.snapshotId }, safeMode: true, unresolved: true };
|
|
280
|
+
}
|
|
281
|
+
// 快照有效 + 已应用部分 + 无不可证明 → rollback 可安全执行(需用户确认)
|
|
282
|
+
await store.writeSafeMode(true).catch(() => undefined);
|
|
283
|
+
const reason = '部分 step 已应用,可安全回滚(需用户确认)';
|
|
284
|
+
return { decision: { operationId, kind: 'rollback-recommended', reason, snapshotId: j.snapshotId }, safeMode: true, unresolved: true };
|
|
285
|
+
}
|
|
286
|
+
export async function executeRecovery(store, input, userConfirmed) {
|
|
287
|
+
const j = await store.loadActive(input.operationId);
|
|
288
|
+
if (j === null)
|
|
289
|
+
return 'failed'; // 不再 active(已处理/隔离)
|
|
290
|
+
if (input.action === 'resume') {
|
|
291
|
+
// resume 不重做已完成副作用:标记 RECOVERED + move
|
|
292
|
+
await store.update(input.operationId, (cur) => {
|
|
293
|
+
let next = cur.state === 'RECOVERED' ? cur : transitionJournalState(cur, 'RECOVERED');
|
|
294
|
+
next = { ...next, recovery: { ...next.recovery, attemptedAt: new Date().toISOString(), outcome: 'RECOVERED', reason: 'user resume' } };
|
|
295
|
+
return next;
|
|
296
|
+
});
|
|
297
|
+
await store.moveToCompleted(input.operationId).catch(() => undefined);
|
|
298
|
+
return 'done';
|
|
299
|
+
}
|
|
300
|
+
if (input.action === 'rollback') {
|
|
301
|
+
if (!userConfirmed)
|
|
302
|
+
return 'needs-confirmation'; // 普通 metadata 不是 destructive 授权
|
|
303
|
+
if (input.snapshotId === null)
|
|
304
|
+
return 'failed'; // 无有效快照不可回滚
|
|
305
|
+
// §11.2:execute 仅接受 NEEDS_ATTENTION(confirm 后)或 RECOVERING(retry 续跑);terminal 拒绝 replay。
|
|
306
|
+
if (j.state !== 'NEEDS_ATTENTION' && j.state !== 'RECOVERING')
|
|
307
|
+
return 'failed';
|
|
308
|
+
// §5.2 引擎映射:rollback-recommended → restore executor;rollback-continue → rollback executor。
|
|
309
|
+
// 不混用两套引擎;缺省 decision 走 rollback executor(兼容旧调用)。
|
|
310
|
+
const executor = input.decision === 'rollback-recommended' ? input.performRestore : input.performRollback;
|
|
311
|
+
if (executor === undefined)
|
|
312
|
+
return 'failed';
|
|
313
|
+
// 真正开始时:NEEDS_ATTENTION → RECOVERING(§4.2/§7.1)。recovery 全程保持 RECOVERING,
|
|
314
|
+
// **不引入 ROLLING_BACK**(ALLOWED_TRANSITIONS['RECOVERING'] 不含 ROLLING_BACK)。
|
|
315
|
+
// retry 续跑时 journal 已 RECOVERING → 幂等保持。
|
|
316
|
+
await store.update(input.operationId, (cur) => {
|
|
317
|
+
if (cur.state === 'RECOVERING')
|
|
318
|
+
return cur;
|
|
319
|
+
if (cur.state === 'NEEDS_ATTENTION')
|
|
320
|
+
return transitionJournalState(cur, 'RECOVERING');
|
|
321
|
+
return cur; // 其它状态(terminal 等)不迁移
|
|
322
|
+
}).catch(() => undefined);
|
|
323
|
+
const report = await executor(input.snapshotId);
|
|
324
|
+
// 执行完成:保持 RECOVERING(记录 rollback 报告;终态迁移 + verification 写入由 verify 路由
|
|
325
|
+
// 以单次原子 journal update 完成,见 §6.5)。此处绝不自行转 ROLLED_BACK。
|
|
326
|
+
await store.update(input.operationId, (cur) => {
|
|
327
|
+
return { ...cur, rollback: { ...cur.rollback, full: report.full, failed: report.failed } };
|
|
328
|
+
}).catch(() => undefined);
|
|
329
|
+
return 'done';
|
|
330
|
+
}
|
|
331
|
+
// dismiss(quarantine & dismiss):移出 active,不视为恢复(用户显式放弃)
|
|
332
|
+
if (!userConfirmed)
|
|
333
|
+
return 'needs-confirmation';
|
|
334
|
+
await store.quarantine(input.operationId, 'user dismissed').catch(() => undefined);
|
|
335
|
+
return 'done';
|
|
336
|
+
}
|
|
337
|
+
// ---------- recovery decision 重算(§5.4) ----------
|
|
338
|
+
/**
|
|
339
|
+
* 对 terminal `NEEDS_ATTENTION` journal 重算 recovery 决策(§5.4,Reviewer A 第二轮 P1-2)。
|
|
340
|
+
*
|
|
341
|
+
* `reconcileActive` 对 terminal `NEEDS_ATTENTION` journal 恒返回 `needs-attention`
|
|
342
|
+
* (`NEEDS_ATTENTION` 是 terminal,`rollback-recommended`/`rollback-continue` 只在转换进
|
|
343
|
+
* NEEDS_ATTENTION 的当次 pass 出现一次)。`GET /recovery/status` 用本函数重算,区分
|
|
344
|
+
* `rollback-recommended` / `rollback-continue` / `needs-attention`。
|
|
345
|
+
*
|
|
346
|
+
* **只读**:不修改 journal,不写 SAFE MODE,不写 recovery-history。
|
|
347
|
+
*/
|
|
348
|
+
export async function recomputeRecoveryDecision(j, hooks) {
|
|
349
|
+
// 回滚中断(entryDone 非空)→ 续跑中断回滚
|
|
350
|
+
if (Object.keys(j.rollback.entryDone ?? {}).length > 0)
|
|
351
|
+
return 'rollback-continue';
|
|
352
|
+
// 有合法且存在的 trusted bound snapshot → 可恢复到快照
|
|
353
|
+
if (j.snapshotId !== null && j.snapshotId !== '') {
|
|
354
|
+
const exists = await hooks.snapshotExists(j.snapshotId, {
|
|
355
|
+
operationId: j.operationId,
|
|
356
|
+
ownerInstanceId: j.ownerInstanceId,
|
|
357
|
+
environmentFingerprint: j.environmentFingerprint,
|
|
358
|
+
});
|
|
359
|
+
if (exists)
|
|
360
|
+
return 'rollback-recommended';
|
|
361
|
+
}
|
|
362
|
+
return 'needs-attention';
|
|
363
|
+
}
|
|
364
|
+
/**
|
|
365
|
+
* Startup 只读事务状态 + 锁状态扫描(Rev 3 P1-NEW-2):
|
|
366
|
+
* 只检测、只读;**不自动 recoverStaleLock、不自动 create journal**。
|
|
367
|
+
* lockState: 'LOCKED' | 'STALE_LOCK_DETECTED' | 'UNKNOWN_STATE' | 'FREE'
|
|
368
|
+
*/
|
|
369
|
+
export async function inspectStartup(store, hooks, env, opts, lockState) {
|
|
370
|
+
// 锁为 LOCKED(fresh heartbeat,活跃 owner)→ 任何 incomplete journal 皆视为 live op,不 reconcile/quarantine/move;
|
|
371
|
+
// 仅 STALE / UNKNOWN / FREE 时才对 journal 做 recover 判定(修 isLiveOwner 并发误判,§18)。
|
|
372
|
+
// 注意:此处用 `||`(不能 `&&`——env.isLiveOwner 在保守 hooks 下恒 false,`LOCKED && false` 会让 live 分支永不到达)。
|
|
373
|
+
const envWithLive = {
|
|
374
|
+
...env,
|
|
375
|
+
isLiveOwner: async (j) => lockState === 'LOCKED' || (await env.isLiveOwner(j)),
|
|
376
|
+
};
|
|
377
|
+
const outcome = await reconcileActive(store, hooks, envWithLive, { ...opts, organizeTerminal: true });
|
|
378
|
+
const hasIncomplete = outcome.unresolved.length > 0;
|
|
379
|
+
let recoveryRequired = false;
|
|
380
|
+
if (hasIncomplete && (lockState === 'STALE_LOCK_DETECTED' || lockState === 'UNKNOWN_STATE')) {
|
|
381
|
+
recoveryRequired = true;
|
|
382
|
+
await store.writeSafeMode(true).catch(() => undefined);
|
|
383
|
+
}
|
|
384
|
+
// 读 durable SAFE MODE 标记(跨重启保持阻断):即使 active 已规整,标记在则仍须阻断
|
|
385
|
+
const durableSafe = await store.readSafeMode().catch(() => false);
|
|
386
|
+
const safeModeRequired = outcome.safeModeRequired || recoveryRequired || durableSafe;
|
|
387
|
+
return { safeModeRequired, recoveryRequired, decisions: outcome.decisions, unresolved: outcome.unresolved };
|
|
388
|
+
}
|
|
389
|
+
//# sourceMappingURL=reconcile.js.map
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { type JournalStore } from './journal.ts';
|
|
2
|
+
import { RunRegistry } from './run-registry.ts';
|
|
3
|
+
import type { HostContext } from './types.ts';
|
|
4
|
+
import type { MsgFunc } from './messages.ts';
|
|
5
|
+
/** 路由注入的恢复执行器(restore / rollback 引擎;测试注入 mock)。 */
|
|
6
|
+
export interface RecoveryExecutorFns {
|
|
7
|
+
/** rollback-recommended:恢复到 trusted snapshot(restore.ts 引擎)。 */
|
|
8
|
+
performRestore: (snapshotId: string) => Promise<{
|
|
9
|
+
full: boolean;
|
|
10
|
+
failed: string[];
|
|
11
|
+
}>;
|
|
12
|
+
/** rollback-continue:续跑中断回滚(rollback.ts 引擎)。 */
|
|
13
|
+
performRollback: (snapshotId: string) => Promise<{
|
|
14
|
+
full: boolean;
|
|
15
|
+
failed: string[];
|
|
16
|
+
}>;
|
|
17
|
+
}
|
|
18
|
+
export interface RecoveryOrchestratorDeps {
|
|
19
|
+
store: JournalStore;
|
|
20
|
+
runs: RunRegistry;
|
|
21
|
+
snapshotsDir: string;
|
|
22
|
+
host: HostContext;
|
|
23
|
+
msg: MsgFunc;
|
|
24
|
+
/** journal↔snapshot 正向校验(phase3Recovery.recoveryHooks.snapshotExists)。 */
|
|
25
|
+
snapshotExists: (snapshotId: string | null, binding?: {
|
|
26
|
+
operationId?: string;
|
|
27
|
+
ownerInstanceId?: string;
|
|
28
|
+
environmentFingerprint?: string;
|
|
29
|
+
}) => Promise<boolean>;
|
|
30
|
+
/**
|
|
31
|
+
* 当前环境指纹(**动态 getter**,非创建时捕获快照)。
|
|
32
|
+
* 原因:宿主启动 recovery 分类在 fire-and-forget 异步块中跑 `initFingerprint()`,
|
|
33
|
+
* 未在 makeRoutes 前 await 完成;若此处捕获创建时的值会拿到 'unknown' 初值,
|
|
34
|
+
* 导致后续所有 recovery API 判 WRONG_ENVIRONMENT(环境指纹不匹配)。动态读取
|
|
35
|
+
* 保证 initFingerprint 完成后,API 调用时取到真实指纹。
|
|
36
|
+
*/
|
|
37
|
+
getEnvironmentFingerprint: () => string;
|
|
38
|
+
/**
|
|
39
|
+
* 清除 SAFE MODE(解除阻断)。宿主注入 `phase3Recovery.clearSafeMode()`:
|
|
40
|
+
* 同时重置内存 `safeModeActive` 标志(isBlocked 读它)与 durable 标记。
|
|
41
|
+
* 仅当 recovery 成功且无其他未解决 incident 时调用(见 maybeClearSafeMode)。
|
|
42
|
+
*/
|
|
43
|
+
clearSafeMode: () => Promise<void>;
|
|
44
|
+
/**
|
|
45
|
+
* 只读环境锁状态(issue #31)。**由宿主注入而非本模块 import**:本模块不依赖 env-lock,
|
|
46
|
+
* 无锁环境(测试 mock 端口)可返回保守的 UNKNOWN_STATE。
|
|
47
|
+
* detail 含 owner pid/op 等内部诊断 → 只用于日志,绝不进响应体。
|
|
48
|
+
*/
|
|
49
|
+
inspectLockState: () => Promise<{
|
|
50
|
+
state: string;
|
|
51
|
+
detail?: string;
|
|
52
|
+
}>;
|
|
53
|
+
/**
|
|
54
|
+
* 显式回收 stale 残留锁(EnvironmentLockManager.recoverStaleLock)。
|
|
55
|
+
* **调用方不得先 acquire 锁**:要回收的正是挡住 acquire 的那把锁,先取锁必然失败。
|
|
56
|
+
* 该方法自身保证 inspect → 证明确证死亡 → 原子 rename 捕获 → 二次验证 → unlink,
|
|
57
|
+
* 任一步不确定即拒绝(活锁绝不删)。
|
|
58
|
+
*/
|
|
59
|
+
recoverStaleLock: () => Promise<{
|
|
60
|
+
ok: boolean;
|
|
61
|
+
removed: boolean;
|
|
62
|
+
state: string;
|
|
63
|
+
detail?: string;
|
|
64
|
+
}>;
|
|
65
|
+
}
|
|
66
|
+
/** 编排结果(路由映射为 HTTP 响应)。 */
|
|
67
|
+
export type RecoveryResult = {
|
|
68
|
+
status: 200;
|
|
69
|
+
body: Record<string, unknown>;
|
|
70
|
+
} | {
|
|
71
|
+
status: 400;
|
|
72
|
+
body: Record<string, unknown>;
|
|
73
|
+
} | {
|
|
74
|
+
status: 404;
|
|
75
|
+
body: Record<string, unknown>;
|
|
76
|
+
} | {
|
|
77
|
+
status: 409;
|
|
78
|
+
body: Record<string, unknown>;
|
|
79
|
+
} | {
|
|
80
|
+
status: 500;
|
|
81
|
+
body: Record<string, unknown>;
|
|
82
|
+
};
|
|
83
|
+
export interface RecoveryOrchestrator {
|
|
84
|
+
status(): Promise<RecoveryResult>;
|
|
85
|
+
preview(operationId: string): Promise<RecoveryResult>;
|
|
86
|
+
confirm(operationId: string, userConfirmed: boolean): Promise<RecoveryResult>;
|
|
87
|
+
execute(operationId: string, userConfirmed: boolean, makeExecutors: (runId: string) => RecoveryExecutorFns): Promise<RecoveryResult>;
|
|
88
|
+
verify(operationId: string): Promise<RecoveryResult>;
|
|
89
|
+
retry(operationId: string, userConfirmed: boolean, makeExecutors: (runId: string) => RecoveryExecutorFns): Promise<RecoveryResult>;
|
|
90
|
+
dismiss(operationId: string, userConfirmed: boolean): Promise<RecoveryResult>;
|
|
91
|
+
/** issue #31:显式回收 stale 残留锁(无 operationId;非 journal 事项)。 */
|
|
92
|
+
recoverStaleLock(userConfirmed: boolean): Promise<RecoveryResult>;
|
|
93
|
+
}
|
|
94
|
+
export declare function createRecoveryOrchestrator(deps: RecoveryOrchestratorDeps): RecoveryOrchestrator;
|