dsh-config-manager 0.1.71 → 0.1.73
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/README.md +149 -98
- package/README.zh-CN.md +245 -88
- package/lib/adapters/agent-instructions.js +1 -1
- package/lib/adapters/file-collection.js +1 -1
- package/lib/adapters/index.d.ts +2 -1
- package/lib/adapters/index.js +6 -1
- package/lib/adapters/link-report.d.ts +1 -0
- package/lib/adapters/link-report.js +27 -3
- package/lib/adapters/messages.d.ts +21 -0
- package/lib/adapters/messages.js +57 -0
- package/lib/adapters/plugin-data.d.ts +114 -0
- package/lib/adapters/plugin-data.js +309 -0
- package/lib/adapters/plugin-files.js +3 -2
- package/lib/adapters/plugins.js +4 -3
- package/lib/adapters/prompts.d.ts +1 -0
- package/lib/adapters/prompts.js +7 -1
- package/lib/adapters/self.d.ts +8 -2
- package/lib/adapters/self.js +3 -2
- package/lib/adapters/sessions.d.ts +1 -0
- package/lib/adapters/sessions.js +4 -2
- package/lib/adapters/test-helpers.d.ts +9 -0
- package/lib/adapters/test-helpers.js +14 -0
- package/lib/adapters/ui.d.ts +2 -2
- package/lib/adapters/ui.js +25 -25
- package/lib/adapters/workspaces.d.ts +1 -0
- package/lib/adapters/workspaces.js +7 -3
- package/lib/cli/actions.d.ts +9 -1
- package/lib/cli/actions.js +5 -2
- package/lib/cli/help.js +23 -10
- package/lib/cli/index.d.ts +23 -0
- package/lib/cli/index.js +90 -32
- package/lib/cli/sessions-inspect.js +3 -1
- package/lib/cli/sessions-repair.js +25 -6
- package/lib/cli/web/page.d.ts +12 -0
- package/lib/cli/web/page.js +21 -12
- package/lib/cli/web/routes.js +2 -2
- package/lib/cli/web/server.js +4 -4
- package/lib/client.d.ts +1442 -494
- package/lib/client.js +15326 -8520
- package/lib/core/analyzer.d.ts +11 -2
- package/lib/core/analyzer.js +31 -8
- package/lib/core/backup-plan.d.ts +10 -0
- package/lib/core/backup-plan.js +51 -20
- package/lib/core/backup-verify.js +24 -23
- package/lib/core/backup.d.ts +25 -1
- package/lib/core/backup.js +99 -28
- package/lib/core/boot-rescue.js +4 -3
- package/lib/core/boot-safety.js +2 -1
- package/lib/core/command-rollback.d.ts +182 -0
- package/lib/core/command-rollback.js +293 -0
- package/lib/core/consult-source.js +7 -6
- package/lib/core/disk-usage.d.ts +74 -10
- package/lib/core/disk-usage.js +67 -4
- package/lib/core/engine-locale.d.ts +87 -0
- package/lib/core/engine-locale.js +125 -0
- package/lib/core/exporter.js +3 -3
- package/lib/core/fork-reconcile.d.ts +128 -0
- package/lib/core/fork-reconcile.js +150 -0
- package/lib/core/journal.js +6 -5
- package/lib/core/local-plugin-host.js +3 -2
- package/lib/core/local-plugin-pack.js +11 -6
- package/lib/core/messages-core.d.ts +294 -0
- package/lib/core/messages-core.js +619 -0
- package/lib/core/messages.d.ts +61 -2
- package/lib/core/messages.js +118 -13
- package/lib/core/migration-consult.js +21 -20
- package/lib/core/migration-history.js +19 -5
- package/lib/core/model-tools.d.ts +0 -22
- package/lib/core/model-tools.js +50 -27
- package/lib/core/phase3-host.js +30 -5
- package/lib/core/plugin-cli.js +18 -19
- package/lib/core/plugin-data-registry.d.ts +124 -0
- package/lib/core/plugin-data-registry.js +198 -0
- package/lib/core/reconcile.d.ts +2 -14
- package/lib/core/reconcile.js +87 -17
- package/lib/core/recovery-orchestrator.js +19 -18
- package/lib/core/reinstall.d.ts +4 -0
- package/lib/core/reinstall.js +26 -21
- package/lib/core/restore.js +20 -17
- package/lib/core/rollback-gate.d.ts +33 -0
- package/lib/core/rollback-gate.js +57 -0
- package/lib/core/rollback-ignore-rules.d.ts +31 -0
- package/lib/core/rollback-ignore-rules.js +140 -0
- package/lib/core/rollback-ledger.d.ts +248 -0
- package/lib/core/rollback-ledger.js +1282 -0
- package/lib/core/rollback-point.d.ts +1391 -0
- package/lib/core/rollback-point.js +3503 -0
- package/lib/core/rollback.js +6 -4
- package/lib/core/run-registry.js +6 -5
- package/lib/core/session-health.d.ts +15 -0
- package/lib/core/session-meta.d.ts +42 -2
- package/lib/core/session-meta.js +140 -1
- package/lib/core/session-title.d.ts +28 -0
- package/lib/core/session-title.js +185 -0
- package/lib/core/startup-barrier.d.ts +24 -0
- package/lib/core/startup-barrier.js +22 -0
- package/lib/core/transaction-coordinator.js +4 -3
- package/lib/core/types.d.ts +37 -1
- package/lib/core/validator.js +5 -4
- package/lib/core/verify-recovery.js +31 -30
- package/lib/foreign/bundle.js +3 -2
- package/lib/foreign/continue.js +2 -1
- package/lib/foreign/crush.js +2 -1
- package/lib/foreign/gemini.js +2 -1
- package/lib/foreign/goose.js +2 -1
- package/lib/foreign/kilocode.js +2 -1
- package/lib/foreign/kimi.js +2 -1
- package/lib/foreign/messages.d.ts +15 -0
- package/lib/foreign/messages.js +40 -0
- package/lib/foreign/mimocode.js +2 -1
- package/lib/foreign/openclaw.js +2 -1
- package/lib/foreign/opencode.js +2 -1
- package/lib/foreign/pi.js +2 -1
- package/lib/foreign/qoder.js +2 -1
- package/lib/foreign/qwen.js +2 -1
- package/lib/foreign/registry.d.ts +1 -1
- package/lib/foreign/registry.js +8 -7
- package/lib/foreign/teleagent.js +2 -1
- package/lib/foreign/workbuddy.js +2 -1
- package/lib/foreign/zcode.js +2 -1
- package/lib/foreign/zed.js +2 -1
- package/lib/index.d.ts +227 -7
- package/lib/index.js +799 -54
- package/lib/market/git-file-writer.js +20 -19
- package/lib/market/github-repos.js +35 -19
- package/lib/market/index-parser.js +31 -30
- package/lib/market/messages-market.d.ts +138 -0
- package/lib/market/messages-market.js +305 -0
- package/lib/market/my-repo.d.ts +2 -2
- package/lib/market/my-repo.js +32 -29
- package/lib/market/prepare.js +18 -15
- package/lib/market/security.js +20 -19
- package/lib/market/types.js +2 -1
- package/lib/market/url.js +2 -1
- package/lib/migrations/index.js +6 -5
- package/lib/migrations/messages-migrations.d.ts +32 -0
- package/lib/migrations/messages-migrations.js +58 -0
- package/lib/profiles/dsh-profile-launcher.js +2 -1
- package/lib/profiles/dsh-profile-runtime.js +2 -1
- package/lib/profiles/messages.d.ts +7 -0
- package/lib/profiles/messages.js +27 -0
- package/lib/routes/backup.js +18 -2
- package/lib/routes/import.js +3 -3
- package/lib/routes/index.d.ts +1 -1
- package/lib/routes/kit.d.ts +3 -2
- package/lib/routes/kit.js +6 -1
- package/lib/routes/market.js +1 -1
- package/lib/routes/messages.d.ts +43 -0
- package/lib/routes/messages.js +102 -0
- package/lib/routes/prefs.js +26 -3
- package/lib/routes/profiles.js +12 -6
- package/lib/routes/recovery.d.ts +21 -0
- package/lib/routes/recovery.js +478 -94
- package/lib/routes/snapshots.js +51 -4
- package/lib/routes/sync.js +79 -17
- package/lib/schema/config.d.ts +11 -1
- package/lib/schema/config.js +52 -20
- package/lib/schema/index.d.ts +8 -6
- package/lib/schema/index.js +8 -6
- package/lib/schema/manifest.js +19 -18
- package/lib/schema/messages.d.ts +47 -0
- package/lib/schema/messages.js +110 -0
- package/lib/schema/section-registry.d.ts +42 -15
- package/lib/schema/section-registry.js +67 -32
- package/lib/schema/types.d.ts +1 -1
- package/lib/schema/versions.js +6 -5
- package/lib/security/encryption.js +16 -15
- package/lib/security/integrity.js +8 -7
- package/lib/security/messages.d.ts +30 -0
- package/lib/security/messages.js +77 -0
- package/lib/security/redaction.js +12 -0
- package/lib/security/secret-scanner.js +16 -13
- package/lib/security/vault.js +2 -1
- package/lib/security/zip-security.js +4 -3
- package/lib/sync/ancestor.js +2 -1
- package/lib/sync/autosync-config.d.ts +11 -0
- package/lib/sync/autosync-config.js +12 -0
- package/lib/sync/autosync-scheduler.d.ts +11 -1
- package/lib/sync/autosync-scheduler.js +115 -76
- package/lib/sync/backup-files.js +3 -2
- package/lib/sync/backup-scheduler.js +6 -5
- package/lib/sync/blob-store.js +2 -1
- package/lib/sync/git/git-transport.d.ts +55 -0
- package/lib/sync/git/git-transport.js +152 -2
- package/lib/sync/github-auth.js +10 -9
- package/lib/sync/layout.js +13 -12
- package/lib/sync/messages.d.ts +63 -0
- package/lib/sync/messages.js +149 -0
- package/lib/sync/review-queue.js +6 -5
- package/lib/sync/s3/messages.d.ts +7 -0
- package/lib/sync/s3/messages.js +16 -0
- package/lib/sync/s3/s3-providers.d.ts +4 -1
- package/lib/sync/s3/s3-providers.js +5 -5
- package/lib/sync/s3/s3-transport.js +1 -1
- package/lib/sync/s3/sigv4.js +2 -1
- package/lib/sync/snapshot-crypto.js +6 -5
- package/lib/sync/snapshot-json.js +3 -2
- package/lib/sync/sync-engine.d.ts +58 -22
- package/lib/sync/sync-engine.js +119 -48
- package/lib/sync/sync-history.js +7 -6
- package/lib/sync/sync-selection.d.ts +15 -4
- package/lib/sync/sync-selection.js +17 -5
- package/lib/sync/ui-prefs.d.ts +22 -0
- package/lib/sync/ui-prefs.js +28 -0
- package/lib/sync/webdav/webdav-transport.d.ts +1 -0
- package/lib/sync/webdav/webdav-transport.js +4 -1
- package/lib/ui/artifact-view.js +4 -3
- package/lib/ui/commands.d.ts +1 -1
- package/lib/ui/commands.js +10 -0
- package/lib/ui/diff-view.d.ts +10 -0
- package/lib/ui/diff-view.js +17 -0
- package/lib/ui/disk-usage-view.d.ts +33 -4
- package/lib/ui/disk-usage-view.js +29 -1
- package/lib/ui/errors.d.ts +1 -1
- package/lib/ui/errors.js +4 -3
- package/lib/ui/export-flow.js +26 -20
- package/lib/ui/foreign-view.d.ts +5 -1
- package/lib/ui/foreign-view.js +26 -2
- package/lib/ui/i18n.d.ts +68 -1
- package/lib/ui/i18n.js +120 -2
- package/lib/ui/import-wizard.d.ts +7 -0
- package/lib/ui/import-wizard.js +13 -10
- package/lib/ui/progress.d.ts +2 -2
- package/lib/ui/progress.js +1 -2
- package/lib/ui/report.d.ts +5 -5
- package/lib/ui/report.js +6 -6
- package/lib/ui/rollback-view.d.ts +815 -0
- package/lib/ui/rollback-view.js +738 -0
- package/lib/ui/selection-model.d.ts +41 -0
- package/lib/ui/selection-model.js +30 -0
- package/lib/ui/session-inventory-view.d.ts +97 -3
- package/lib/ui/session-inventory-view.js +76 -2
- package/lib/ui/session-issue-labels.d.ts +43 -0
- package/lib/ui/session-issue-labels.js +48 -0
- package/lib/ui/sync-settings-view.d.ts +33 -0
- package/lib/ui/sync-settings-view.js +40 -0
- package/lib/ui/test-helpers.js +2 -1
- package/lib/ui/types.d.ts +8 -3
- package/lib/ui/types.js +1 -1
- package/lib/utils/atomic-write.js +6 -5
- package/lib/utils/bundle-scan.d.ts +11 -1
- package/lib/utils/bundle-scan.js +12 -1
- package/lib/utils/env-lock.d.ts +10 -0
- package/lib/utils/env-lock.js +73 -46
- package/lib/utils/json.js +6 -5
- package/lib/utils/messages-utils.d.ts +96 -0
- package/lib/utils/messages-utils.js +216 -0
- package/lib/utils/proxy.js +18 -5
- package/lib/utils/recursive-walk.d.ts +22 -0
- package/lib/utils/recursive-walk.js +75 -1
- package/lib/utils/session-log-repair.js +10 -9
- package/lib/utils/zip.js +27 -26
- package/package.json +12 -2
- package/src/adapters/agent-instructions.ts +1 -1
- package/src/adapters/file-collection.test.ts +1 -0
- package/src/adapters/file-collection.ts +1 -1
- package/src/adapters/files.test.ts +27 -0
- package/src/adapters/index.ts +6 -1
- package/src/adapters/link-report.test.ts +22 -2
- package/src/adapters/link-report.ts +29 -4
- package/src/adapters/messages.ts +61 -0
- package/src/adapters/plugin-data.test.ts +225 -0
- package/src/adapters/plugin-data.ts +351 -0
- package/src/adapters/plugin-files.ts +3 -2
- package/src/adapters/plugins.ts +4 -3
- package/src/adapters/prompts.ts +7 -1
- package/src/adapters/self.test.ts +26 -0
- package/src/adapters/self.ts +10 -3
- package/src/adapters/sessions.ts +4 -2
- package/src/adapters/settings.test.ts +38 -1
- package/src/adapters/test-helpers.ts +15 -0
- package/src/adapters/ui.test.ts +2 -2
- package/src/adapters/ui.ts +27 -26
- package/src/adapters/workspaces.ts +7 -3
- package/src/cli/actions.test.ts +28 -0
- package/src/cli/actions.ts +11 -2
- package/src/cli/help.ts +23 -10
- package/src/cli/index-guards.test.ts +193 -0
- package/src/cli/index.ts +106 -31
- package/src/cli/sessions-inspect.test.ts +15 -1
- package/src/cli/sessions-inspect.ts +3 -1
- package/src/cli/sessions-repair.test.ts +84 -0
- package/src/cli/sessions-repair.ts +33 -7
- package/src/cli/web/console-route-parity.test.ts +115 -0
- package/src/cli/web/page.ts +23 -12
- package/src/cli/web/routes.ts +2 -1
- package/src/cli/web/server.ts +4 -4
- package/src/cli/web/web.test.ts +2 -2
- package/src/client/ConfigManagerSection.tsx +30 -2
- package/src/client/about/AboutPanel.tsx +2 -2
- package/src/client/about/PluginUpdateDialog.tsx +1 -1
- package/src/client/about/ReleaseNotesDialog.tsx +2 -2
- package/src/client/about/about-view.test.ts +13 -3
- package/src/client/about/about-view.ts +10 -6
- package/src/client/about/release-notes-view.test.ts +14 -4
- package/src/client/about/release-notes-view.ts +4 -2
- package/src/client/api.ts +18 -6
- package/src/client/chat/ChatEntryView.tsx +411 -0
- package/src/client/chat/RollbackDiffTab.tsx +108 -0
- package/src/client/chat/RollbackFileIcon.tsx +96 -0
- package/src/client/chat/RollbackReviewDialog.tsx +607 -0
- package/src/client/chat/chat-entry-view.test.ts +1174 -0
- package/src/client/chat/chat-entry-view.ts +758 -0
- package/src/client/chat/chat-locales.ts +152 -0
- package/src/client/chat/chat-node-registration.ts +64 -0
- package/src/client/chat/chat-point-window.ts +183 -0
- package/src/client/chat/chat-slots.ts +107 -0
- package/src/client/chat/rollback-diff-tab.test.ts +105 -0
- package/src/client/chat/rollback-diff-tab.ts +174 -0
- package/src/client/chat/rollback-entry-view.test.ts +1469 -0
- package/src/client/chat/rollback-entry-view.ts +946 -0
- package/src/client/chat/sidebar-slots.ts +59 -0
- package/src/client/common/ConfirmDialog.tsx +7 -3
- package/src/client/common/ContentPicker.tsx +48 -10
- package/src/client/common/DiffTable.tsx +133 -0
- package/src/client/common/ErrorBanner.tsx +4 -4
- package/src/client/common/Icon.tsx +2 -0
- package/src/client/common/Modal.tsx +8 -1
- package/src/client/common/ProgressBar.tsx +8 -2
- package/src/client/common/ReportView.tsx +4 -4
- package/src/client/common/RunsCenter.tsx +1 -0
- package/src/client/common/client-route-parity.test.ts +84 -0
- package/src/client/common/info-hint-guard.test.ts +46 -13
- package/src/client/common/picker-lazy-units.test.ts +42 -0
- package/src/client/common/plan-text-redaction.test.ts +17 -6
- package/src/client/common/progress-view-edge.test.ts +8 -7
- package/src/client/common/progress-view.test.ts +9 -9
- package/src/client/common/progress-view.ts +2 -2
- package/src/client/common/routes.ts +38 -0
- package/src/client/common/section-labels.test.ts +1 -0
- package/src/client/common/section-labels.ts +2 -0
- package/src/client/common/ui.tsx +73 -10
- package/src/client/config-manager.module.css +1109 -31
- package/src/client/css-scale-guard.test.ts +30 -0
- package/src/client/environment/EnvironmentPanel.tsx +11 -4
- package/src/client/environment/maintenance/DiskUsageCard.tsx +23 -2
- package/src/client/export/ExportView.tsx +13 -8
- package/src/client/history/HistoryPanel.tsx +4 -3
- package/src/client/history/history-api.ts +2 -2
- package/src/client/history/history-locales.ts +5 -1
- package/src/client/home/HomePanel.tsx +29 -8
- package/src/client/import/ForeignImportView.tsx +4 -5
- package/src/client/import/ImportWizardView.tsx +5 -3
- package/src/client/import/import-wizard-split.test.ts +3 -1
- package/src/client/import/import-wizard-steps.tsx +9 -9
- package/src/client/index.ts +232 -3
- package/src/client/library/LibraryActions.tsx +32 -7
- package/src/client/library/LibraryPanel.tsx +23 -3
- package/src/client/library/RestorePlanView.tsx +9 -66
- package/src/client/locales.ts +62 -9
- package/src/client/lucide-icons.d.ts +1 -0
- package/src/client/market/MarketPanel.tsx +13 -2
- package/src/client/market/MyConfigsView.tsx +5 -4
- package/src/client/market/MyConfigsWizard.tsx +10 -8
- package/src/client/market/market-api.test.ts +9 -8
- package/src/client/market/market-api.ts +2 -2
- package/src/client/market/market-locales.ts +3 -0
- package/src/client/market/market-view.test.ts +6 -6
- package/src/client/market/market-view.ts +3 -3
- package/src/client/market/my-configs-api.ts +2 -2
- package/src/client/market/my-configs-view.test.ts +23 -22
- package/src/client/market/my-configs-view.ts +8 -8
- package/src/client/recovery/IncompleteCopiesSection.tsx +2 -1
- package/src/client/recovery/RecoveryPanel.tsx +442 -283
- package/src/client/recovery/RollbackCard.tsx +859 -0
- package/src/client/recovery/RollbackDiffPane.tsx +69 -0
- package/src/client/recovery/RollbackFileChanges.tsx +163 -0
- package/src/client/recovery/incident-api.ts +2 -2
- package/src/client/recovery/recovery-api.ts +144 -6
- package/src/client/recovery/recovery-locales.test.ts +299 -0
- package/src/client/recovery/recovery-locales.ts +520 -149
- package/src/client/recovery/recovery-panel-layout.test.ts +132 -2
- package/src/client/recovery/rollback-enabled.ts +41 -0
- package/src/client/recovery/rollback-file-changes.test.ts +114 -0
- package/src/client/recovery/session-dialog-mount.test.ts +54 -0
- package/src/client/run-store.ts +84 -21
- package/src/client/sync/AutosyncCard.tsx +25 -1
- package/src/client/sync/ChannelClearConfirmDialog.tsx +6 -3
- package/src/client/sync/ChannelConfigDialog.tsx +10 -10
- package/src/client/sync/DecryptPasswordCard.tsx +2 -2
- package/src/client/sync/SecurityOptionsCard.tsx +27 -17
- package/src/client/sync/SyncChannelEntryCard.tsx +14 -2
- package/src/client/sync/SyncConfirmView.tsx +46 -8
- package/src/client/sync/SyncLogList.tsx +1 -1
- package/src/client/sync/SyncPanel.tsx +31 -23
- package/src/client/sync/SyncRepositoryPicker.tsx +3 -5
- package/src/client/sync/SyncSectionPickerDialog.tsx +11 -2
- package/src/client/sync/SyncSettingsView.test.ts +20 -1
- package/src/client/sync/UnreadableSnapshotsBanner.tsx +1 -1
- package/src/client/sync/WorkDirCard.tsx +112 -0
- package/src/client/sync/history-model.test.ts +42 -9
- package/src/client/sync/history-model.ts +30 -15
- package/src/client/sync/sync-api.test.ts +57 -33
- package/src/client/sync/sync-api.ts +80 -6
- package/src/client/sync/sync-locales.ts +104 -3
- package/src/client/sync/sync-view-v2.test.ts +65 -13
- package/src/client/sync/sync-view.test.ts +107 -52
- package/src/client/sync/sync-view.ts +124 -41
- package/src/core/analyzer.ts +31 -9
- package/src/core/backup-plan.test.ts +48 -5
- package/src/core/backup-plan.ts +51 -20
- package/src/core/backup-verify.ts +24 -23
- package/src/core/backup.test.ts +76 -0
- package/src/core/backup.ts +105 -29
- package/src/core/boot-rescue.ts +4 -3
- package/src/core/boot-safety.ts +2 -1
- package/src/core/command-rollback.test.ts +536 -0
- package/src/core/command-rollback.ts +406 -0
- package/src/core/consult-source.ts +7 -6
- package/src/core/disk-usage.test.ts +257 -1
- package/src/core/disk-usage.ts +134 -14
- package/src/core/engine-locale.test.ts +593 -0
- package/src/core/engine-locale.ts +175 -0
- package/src/core/exporter.ts +3 -3
- package/src/core/fork-reconcile.test.ts +279 -0
- package/src/core/fork-reconcile.ts +234 -0
- package/src/core/journal.ts +6 -5
- package/src/core/local-plugin-host.ts +3 -2
- package/src/core/local-plugin-pack.ts +11 -6
- package/src/core/messages-core.ts +661 -0
- package/src/core/messages.ts +125 -17
- package/src/core/migration-consult.ts +21 -20
- package/src/core/migration-history.ts +19 -5
- package/src/core/model-tools.ts +28 -27
- package/src/core/phase3-host.ts +21 -5
- package/src/core/phase3-p2b-fingerprint.test.ts +22 -0
- package/src/core/plugin-cli.ts +18 -19
- package/src/core/plugin-data-registry.test.ts +113 -0
- package/src/core/plugin-data-registry.ts +261 -0
- package/src/core/reconcile.test.ts +117 -0
- package/src/core/reconcile.ts +85 -17
- package/src/core/recovery-orchestrator.ts +19 -18
- package/src/core/reinstall.ts +26 -21
- package/src/core/restore.ts +20 -17
- package/src/core/rollback-file-diff.test.ts +159 -0
- package/src/core/rollback-gate.test.ts +66 -0
- package/src/core/rollback-gate.ts +61 -0
- package/src/core/rollback-ignore-rules.ts +126 -0
- package/src/core/rollback-ledger.test.ts +655 -0
- package/src/core/rollback-ledger.ts +1314 -0
- package/src/core/rollback-point.test.ts +2853 -0
- package/src/core/rollback-point.ts +4340 -0
- package/src/core/rollback.ts +5 -3
- package/src/core/run-registry.ts +6 -5
- package/src/core/session-health.ts +15 -0
- package/src/core/session-meta.test.ts +117 -1
- package/src/core/session-meta.ts +149 -2
- package/src/core/session-title.test.ts +127 -0
- package/src/core/session-title.ts +189 -0
- package/src/core/startup-barrier.test.ts +69 -0
- package/src/core/startup-barrier.ts +27 -0
- package/src/core/transaction-coordinator.ts +4 -3
- package/src/core/types.ts +37 -1
- package/src/core/validator.ts +5 -4
- package/src/core/verify-recovery.ts +31 -30
- package/src/foreign/bundle.ts +3 -2
- package/src/foreign/continue.ts +2 -1
- package/src/foreign/crush.ts +2 -1
- package/src/foreign/file-budget.test.ts +2 -0
- package/src/foreign/gemini.ts +2 -1
- package/src/foreign/goose.ts +2 -1
- package/src/foreign/kilocode.ts +2 -1
- package/src/foreign/kimi.ts +2 -1
- package/src/foreign/messages.ts +44 -0
- package/src/foreign/mimocode.ts +2 -1
- package/src/foreign/openclaw.ts +2 -1
- package/src/foreign/opencode.ts +2 -1
- package/src/foreign/pi.ts +2 -1
- package/src/foreign/qoder.ts +2 -1
- package/src/foreign/qwen.ts +2 -1
- package/src/foreign/registry.test.ts +1 -1
- package/src/foreign/registry.ts +8 -7
- package/src/foreign/teleagent.ts +2 -1
- package/src/foreign/workbuddy.ts +2 -1
- package/src/foreign/zcode.ts +2 -1
- package/src/foreign/zed.ts +2 -1
- package/src/index.facade.test.ts +340 -1
- package/src/index.ts +876 -57
- package/src/market/git-file-writer.ts +20 -19
- package/src/market/github-repos.test.ts +23 -0
- package/src/market/github-repos.ts +37 -19
- package/src/market/index-parser.ts +31 -30
- package/src/market/messages-market.ts +325 -0
- package/src/market/my-repo.ts +32 -29
- package/src/market/prepare.ts +18 -15
- package/src/market/security.ts +20 -19
- package/src/market/types.ts +2 -1
- package/src/market/url.ts +2 -1
- package/src/migrations/index.ts +6 -5
- package/src/migrations/messages-migrations.ts +59 -0
- package/src/profiles/dsh-profile-launcher.ts +2 -1
- package/src/profiles/dsh-profile-runtime.ts +2 -1
- package/src/profiles/messages.ts +31 -0
- package/src/routes/backup.ts +18 -2
- package/src/routes/import.ts +3 -3
- package/src/routes/index.ts +1 -1
- package/src/routes/kit.ts +9 -3
- package/src/routes/market.ts +1 -1
- package/src/routes/messages.ts +106 -0
- package/src/routes/prefs.ts +31 -2
- package/src/routes/profiles.ts +12 -6
- package/src/routes/recovery.test.ts +405 -1
- package/src/routes/recovery.ts +477 -94
- package/src/routes/snapshots.ts +51 -4
- package/src/routes/sync.ts +82 -17
- package/src/schema/config.ts +53 -20
- package/src/schema/index.ts +7 -5
- package/src/schema/manifest-schema.test.ts +4 -4
- package/src/schema/manifest.ts +19 -18
- package/src/schema/messages.ts +120 -0
- package/src/schema/registry.test.ts +28 -7
- package/src/schema/section-registry.ts +96 -42
- package/src/schema/types.ts +2 -1
- package/src/schema/versions.ts +6 -5
- package/src/security/encryption.ts +16 -15
- package/src/security/integrity.ts +8 -7
- package/src/security/messages.ts +87 -0
- package/src/security/redaction.test.ts +17 -1
- package/src/security/redaction.ts +13 -0
- package/src/security/secret-scanner.ts +19 -12
- package/src/security/security.test.ts +47 -0
- package/src/security/vault.ts +2 -1
- package/src/security/zip-security.ts +4 -3
- package/src/sync/ancestor.ts +2 -1
- package/src/sync/autosync-config.ts +21 -0
- package/src/sync/autosync-scheduler.test.ts +63 -7
- package/src/sync/autosync-scheduler.ts +118 -78
- package/src/sync/backup-files.ts +3 -2
- package/src/sync/backup-scheduler.ts +6 -5
- package/src/sync/blob-store.ts +2 -1
- package/src/sync/git/git-transport.test.ts +170 -8
- package/src/sync/git/git-transport.ts +176 -2
- package/src/sync/github-auth.ts +10 -9
- package/src/sync/layout.ts +13 -12
- package/src/sync/messages.ts +153 -0
- package/src/sync/review-queue.ts +6 -5
- package/src/sync/s3/messages.ts +16 -0
- package/src/sync/s3/s3-providers.ts +9 -6
- package/src/sync/s3/s3-transport.ts +1 -1
- package/src/sync/s3/sigv4.ts +3 -1
- package/src/sync/snapshot-crypto.ts +6 -5
- package/src/sync/snapshot-json.ts +3 -2
- package/src/sync/sync-credentials.test.ts +119 -0
- package/src/sync/sync-engine-download-tombstone.test.ts +1 -1
- package/src/sync/sync-engine.test.ts +226 -8
- package/src/sync/sync-engine.ts +154 -52
- package/src/sync/sync-history.ts +7 -6
- package/src/sync/sync-selection.test.ts +12 -2
- package/src/sync/sync-selection.ts +18 -5
- package/src/sync/transport.test.ts +1 -0
- package/src/sync/ui-prefs.test.ts +44 -1
- package/src/sync/ui-prefs.ts +39 -0
- package/src/sync/webdav/webdav-transport.ts +4 -1
- package/src/ui/artifact-view.ts +4 -3
- package/src/ui/commands.test.ts +1 -0
- package/src/ui/commands.ts +11 -1
- package/src/ui/diff-view.test.ts +9 -1
- package/src/ui/diff-view.ts +22 -0
- package/src/ui/disk-usage-view.test.ts +113 -6
- package/src/ui/disk-usage-view.ts +63 -7
- package/src/ui/errors.ts +4 -3
- package/src/ui/export-flow.ts +26 -20
- package/src/ui/foreign-view.test.ts +43 -0
- package/src/ui/foreign-view.ts +30 -3
- package/src/ui/i18n.ts +131 -2
- package/src/ui/import-wizard.ts +18 -10
- package/src/ui/progress.ts +2 -2
- package/src/ui/report.ts +7 -6
- package/src/ui/rollback-view.test.ts +836 -0
- package/src/ui/rollback-view.ts +1405 -0
- package/src/ui/selection-model.test.ts +33 -1
- package/src/ui/selection-model.ts +53 -0
- package/src/ui/session-inventory-view.test.ts +109 -0
- package/src/ui/session-inventory-view.ts +138 -3
- package/src/ui/session-issue-labels.test.ts +51 -0
- package/src/ui/session-issue-labels.ts +70 -0
- package/src/ui/sync-settings-view.test.ts +36 -0
- package/src/ui/sync-settings-view.ts +49 -0
- package/src/ui/test-helpers.ts +2 -1
- package/src/ui/types.ts +10 -4
- package/src/utils/atomic-write.ts +6 -5
- package/src/utils/bundle-scan.test.ts +2 -2
- package/src/utils/bundle-scan.ts +12 -1
- package/src/utils/env-lock.ts +83 -49
- package/src/utils/json.ts +7 -5
- package/src/utils/messages-utils.ts +232 -0
- package/src/utils/proxy.test.ts +25 -0
- package/src/utils/proxy.ts +19 -5
- package/src/utils/recursive-walk.test.ts +43 -0
- package/src/utils/recursive-walk.ts +75 -1
- package/src/utils/session-log-repair.ts +10 -9
- package/src/utils/zip.ts +27 -26
- package/lib/core/checkpoint.d.ts +0 -362
- package/lib/core/checkpoint.js +0 -1309
- package/src/core/checkpoint.test.ts +0 -665
- package/src/core/checkpoint.ts +0 -1614
|
@@ -0,0 +1,1405 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* rollback-point(**工作区 + 会话回滚**)的**展示模型**(框架无关纯函数,node 可测)。
|
|
3
|
+
*
|
|
4
|
+
* 数据来自宿主 /recovery/rollback-points 族的 6 条子路径(list / storage / <id> / <id>/preview /
|
|
5
|
+
* <id>/rewind / <id>/delete;第 3 条 <id> 单条详情同样进端口 —— 面板要能深链到某个点)。
|
|
6
|
+
* 宿主把它挂在既有 recovery **prefix 路由**内部按 path 分发,因此不新增注册路由条目。
|
|
7
|
+
*
|
|
8
|
+
* 三条纪律(与旧检查点展示模型同源,改名不改口径):
|
|
9
|
+
* ① **不 import src/core/rollback-point.ts(或任何 core 模块)** —— 那条链经 node:fs 依赖宿主运行时,
|
|
10
|
+
* 一旦进 client bundle,DSH loader 会报 missed the module table(AGENTS.md 的 client bundle 铁律)。
|
|
11
|
+
* 因此这里按「线协议结构子集」重新声明类型(字段只取展示所需,缺字段一律回落安全值)。
|
|
12
|
+
* ② **本模块不产出任何用户可见文案**:只回传字典键(rollback.*),zh/en 在 recovery-locales.ts。
|
|
13
|
+
* ③ **未知码 / 未知态 / 未知来源一律回落 unknown 键,绝不裸渲染机器码**;用户自由文本(note)与
|
|
14
|
+
* 宿主技术细节(detail / guidance.detail / 路径清单)一律先过 redact()(AGENTS.md UI 硬性规则 7)。
|
|
15
|
+
*
|
|
16
|
+
* 两态纪律(S2):只有 `workspace` 与 `session`。第三态 `config` 与「同点判定」已随检查点一起删除:
|
|
17
|
+
* 任何非两态的 state 字符串都回落 rollback.state.unknown,本模块**不出现** config 态。
|
|
18
|
+
*
|
|
19
|
+
* 线协议形状(与 T1 宿主侧 src/core/rollback-point.ts 逐字段对齐,2026-10-10 双人确认):
|
|
20
|
+
* - points[] = { id, createdAt, sessionId, turn, kind, protected, incomplete, note, spreadMs, segments:[…] },
|
|
21
|
+
* segments **恒为 [workspace, session] 两项**;
|
|
22
|
+
* - preview.workspace = { overwrite, different, missing, createdAfter(数), leftovers(路径清单,≤20), leftoversTruncated },
|
|
23
|
+
* preview.session = { from, to, turns, cursorNow, cursorTarget, restorable, active, reasonCode }
|
|
24
|
+
* (from = 目标回合开始前最后一个事件的 seq,即 fork 的 inclusive 边界;turns = 回合号);
|
|
25
|
+
* - rewind.segments[] = { state, status, reasonCode?, detail[], restored, failed, leftovers[], leftoversTruncated, childSessionId?, guidance? }。
|
|
26
|
+
*/
|
|
27
|
+
import { redact } from '../security/redaction.ts'
|
|
28
|
+
import type { DiffHunk } from '../utils/line-diff.ts'
|
|
29
|
+
|
|
30
|
+
/* ------------------------------------------------------------ 两态 / 三选一 mode */
|
|
31
|
+
|
|
32
|
+
/** 只保留两态(S2):工作区 + 会话。 */
|
|
33
|
+
export const ROLLBACK_STATES = ['workspace', 'session'] as const
|
|
34
|
+
export type RollbackState = (typeof ROLLBACK_STATES)[number]
|
|
35
|
+
|
|
36
|
+
/** 段名 → 是否为受支持的段(出现别的段一律按未知处理,绝不假设它是第三态)。 */
|
|
37
|
+
export function isRollbackState(value: unknown): value is RollbackState {
|
|
38
|
+
return value === 'workspace' || value === 'session'
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* 回退范围三选一(冻结 wire contract 的 `mode`):
|
|
43
|
+
* - `files+session`:工作区文件 + 会话(新子会话)一起回退;
|
|
44
|
+
* - `files`:只回退工作区文件;
|
|
45
|
+
* - `session`:只回退会话(fork 子会话)。
|
|
46
|
+
*/
|
|
47
|
+
export const ROLLBACK_MODES = ['files+session', 'files', 'session'] as const
|
|
48
|
+
export type RollbackMode = (typeof ROLLBACK_MODES)[number]
|
|
49
|
+
|
|
50
|
+
export function isRollbackMode(value: unknown): value is RollbackMode {
|
|
51
|
+
return value === 'files+session' || value === 'files' || value === 'session'
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** mode → 段集合(workspace 段 = 工作区文件;session 段 = 会话)。 */
|
|
55
|
+
export function rollbackModeSegments(mode: string | null | undefined): RollbackState[] {
|
|
56
|
+
if (mode === 'files') return ['workspace']
|
|
57
|
+
if (mode === 'session') return ['session']
|
|
58
|
+
if (mode === 'files+session') return ['workspace', 'session']
|
|
59
|
+
return []
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* 该 mode 在当前可回退段集合下**是否可选**(界面据此禁用注定失败的选项)。
|
|
64
|
+
*
|
|
65
|
+
* 例:某点没有稳定边界(session 段 restorable=false)时,`session` 与 `files+session` 都不可选 ——
|
|
66
|
+
* 绝不发一个宿主一定会拒绝的请求。
|
|
67
|
+
*/
|
|
68
|
+
export function rollbackModeAvailable(mode: string | null | undefined, availableSegments: readonly string[]): boolean {
|
|
69
|
+
const needed = rollbackModeSegments(mode)
|
|
70
|
+
return needed.length > 0 && needed.every((state) => availableSegments.includes(state))
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** 段集合 → mode(三选一;空集合回落 `files+session`,因为那正是自动点缺省该回退的两态)。 */
|
|
74
|
+
export function rollbackModeFromSegments(segments: readonly string[]): RollbackMode {
|
|
75
|
+
const hasWorkspace = segments.includes('workspace')
|
|
76
|
+
const hasSession = segments.includes('session')
|
|
77
|
+
if (hasWorkspace && !hasSession) return 'files'
|
|
78
|
+
if (hasSession && !hasWorkspace) return 'session'
|
|
79
|
+
return 'files+session'
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/* ------------------------------------------------------------ 字典键映射(绝不裸渲染) */
|
|
83
|
+
|
|
84
|
+
export const ROLLBACK_STATE_UNKNOWN_KEY = 'rollback.state.unknown'
|
|
85
|
+
export const ROLLBACK_MODE_UNKNOWN_KEY = 'rollback.mode.unknown'
|
|
86
|
+
export const ROLLBACK_CODE_UNKNOWN_KEY = 'rollback.code.unknown'
|
|
87
|
+
export const ROLLBACK_GUIDANCE_UNKNOWN_KEY = 'rollback.guidance.unknown'
|
|
88
|
+
export const ROLLBACK_KIND_UNKNOWN_KEY = 'rollback.kind.unknown'
|
|
89
|
+
|
|
90
|
+
const STATE_KEYS: Record<RollbackState, string> = {
|
|
91
|
+
workspace: 'rollback.state.workspace',
|
|
92
|
+
session: 'rollback.state.session',
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const MODE_KEYS: Record<RollbackMode, string> = {
|
|
96
|
+
'files+session': 'rollback.mode.filesAndSession',
|
|
97
|
+
files: 'rollback.mode.filesOnly',
|
|
98
|
+
session: 'rollback.mode.sessionOnly',
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* code 白名单:**只有这些码进字典**,其余一律回落 unknown ——
|
|
103
|
+
* 宿主若新增码而字典没跟上,界面显示「未识别的错误码」而不是把机器码拼上屏。
|
|
104
|
+
*
|
|
105
|
+
* 与宿主引擎 `src/core/rollback-point.ts` 的 `RollbackCode` 联合**逐条对应**(core 不能进
|
|
106
|
+
* client bundle,所以只能在这里镜像一份;t3 已按 T1 的最终码表补齐 31 条,t10 补
|
|
107
|
+
* `no-recorded-files` ⇒ 32 条)。顶层 code 与 segments[].reasonCode / warnings 共用同一枚举,
|
|
108
|
+
* 所以一份白名单同时服务三处渲染。
|
|
109
|
+
*
|
|
110
|
+
* ⚠️ **这份镜像与引擎码表的等价性有守卫**:`rollback-view.test.ts` 的「码表镜像 != 引擎码表即红」
|
|
111
|
+
* 用例从 `src/core/rollback-point.ts` 源码级读出 `RollbackCode` union,双向断言集合相等
|
|
112
|
+
* (引擎新增码忘了补这里、或这里多写了引擎没有的码,都会立刻变红 —— 本次 t10 的
|
|
113
|
+
* `no-recorded-files` 就是因为缺这条对照而静默漂移过一轮)。
|
|
114
|
+
*/
|
|
115
|
+
export const ROLLBACK_CODES: readonly string[] = [
|
|
116
|
+
'ok',
|
|
117
|
+
'deleted',
|
|
118
|
+
// 冻结契约的 9 条
|
|
119
|
+
'confirmation-required',
|
|
120
|
+
'protected-point',
|
|
121
|
+
'record-not-found',
|
|
122
|
+
'record-incomplete',
|
|
123
|
+
'invalid-input',
|
|
124
|
+
// 待删的「该点未记录」路径超上限 ⇒ 整套拒绝、零写入(绝不删一半;顶层 code,不是泛化的 invalid-input)
|
|
125
|
+
'created-removals-limit',
|
|
126
|
+
'no-boundary',
|
|
127
|
+
'storage-unavailable',
|
|
128
|
+
'mutation-locked',
|
|
129
|
+
'method-not-allowed',
|
|
130
|
+
// 台账 / 存储
|
|
131
|
+
'ledger-unreadable',
|
|
132
|
+
'ledger-write-failed',
|
|
133
|
+
// 工作区写回(记录校验 / 对象仓 / 写后复验 / 无事可做)
|
|
134
|
+
'invalid-path',
|
|
135
|
+
'object-missing',
|
|
136
|
+
'object-hash-mismatch',
|
|
137
|
+
'restore-failed',
|
|
138
|
+
'verify-failed',
|
|
139
|
+
'nothing-to-restore',
|
|
140
|
+
// T10:该态**没有记录任何文件**(工作区段 files=0)—— 与「记录了但全部已一致」严格分开,
|
|
141
|
+
// 只出现在 segments[].reasonCode(不是顶层 code);界面据此说两句不同的话(t11)。
|
|
142
|
+
'no-recorded-files',
|
|
143
|
+
// 救援点(S5 guard)
|
|
144
|
+
'guard-failed',
|
|
145
|
+
// 会话(fork 子会话;原会话日志只读)
|
|
146
|
+
'session-missing',
|
|
147
|
+
'session-out-of-root',
|
|
148
|
+
'session-too-large',
|
|
149
|
+
'session-active',
|
|
150
|
+
// 子代理会话(origin=subagent):**不自动留点** —— 出现即代表该会话被有意跳过(宿主日志留痕)
|
|
151
|
+
'subagent-session',
|
|
152
|
+
'session-fork-unavailable',
|
|
153
|
+
'fork-failed',
|
|
154
|
+
// 自动留点(工作区快照)
|
|
155
|
+
'capture-failed',
|
|
156
|
+
'capture-truncated',
|
|
157
|
+
'capture-degraded',
|
|
158
|
+
'no-chunks',
|
|
159
|
+
'chunk-not-absolute',
|
|
160
|
+
'chunk-not-found',
|
|
161
|
+
// 单文件差异(「查看」的数据源):该路径**不在**这个回滚点的记录里
|
|
162
|
+
'file-not-recorded',
|
|
163
|
+
]
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* 已知指引码后缀(T1 引擎实际产出;白名单外回落 unknown)。
|
|
167
|
+
*
|
|
168
|
+
* 引擎回传的是**完整字典键**(rollback.guidance.workspaceRolledBack),旧检查点引擎回传的是
|
|
169
|
+
* **裸后缀**(workspacePartial)—— 映射两种形态都认,避免把已知指引误判成 unknown
|
|
170
|
+
* (那会让「工作区已回滚 / 已 fork 子会话」这类关键事实消失)。
|
|
171
|
+
*/
|
|
172
|
+
export const ROLLBACK_GUIDANCE_CODES: readonly string[] = [
|
|
173
|
+
'mountStorage', 'fixDataDir', 'readOnlyFallback', 'probeDetail',
|
|
174
|
+
'workspaceRolledBack', 'forkUnavailable', 'sessionForked', 'sessionNotAttached',
|
|
175
|
+
]
|
|
176
|
+
|
|
177
|
+
/** 已知告警码后缀(warnings 走独立一族 rollback.warn.*;白名单外回落 rollback.warn.unknown)。 */
|
|
178
|
+
export const ROLLBACK_WARN_CODES: readonly string[] = [
|
|
179
|
+
'ledgerUnreadable', 'trackMissing', 'trackDegraded', 'protected', 'guardFailed',
|
|
180
|
+
// t2:删除集被整体拒绝的**成因**(忽略目录 / 路径索引缺失 / 目录读不出)—— 必须能说清为什么没删
|
|
181
|
+
'createdRefused',
|
|
182
|
+
]
|
|
183
|
+
|
|
184
|
+
export const ROLLBACK_WARN_UNKNOWN_KEY = 'rollback.warn.unknown'
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* 指引 / 告警码 → 字典键。三种形态都认:
|
|
188
|
+
* - 完整键 `rollback.guidance.X` / `rollback.warn.X`(T1 引擎);
|
|
189
|
+
* - 裸后缀 `X`(旧检查点引擎)。
|
|
190
|
+
* 未知一律回落 unknown 键,**绝不裸渲染机器码**。
|
|
191
|
+
*/
|
|
192
|
+
function rollbackNoticeKey(code: unknown): string {
|
|
193
|
+
if (typeof code !== 'string' || code === '') return ROLLBACK_GUIDANCE_UNKNOWN_KEY
|
|
194
|
+
if (code.startsWith('rollback.warn.')) {
|
|
195
|
+
const bare = code.slice('rollback.warn.'.length)
|
|
196
|
+
return ROLLBACK_WARN_CODES.includes(bare) ? 'rollback.warn.' + bare : ROLLBACK_WARN_UNKNOWN_KEY
|
|
197
|
+
}
|
|
198
|
+
const bare = code.startsWith('rollback.guidance.') ? code.slice('rollback.guidance.'.length) : code
|
|
199
|
+
return ROLLBACK_GUIDANCE_CODES.includes(bare) ? 'rollback.guidance.' + bare : ROLLBACK_GUIDANCE_UNKNOWN_KEY
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/** 段名 → 字典键(非两态回落 unknown,绝不裸渲染)。 */
|
|
203
|
+
export function rollbackStateKey(state: string | null | undefined): string {
|
|
204
|
+
return isRollbackState(state) ? STATE_KEYS[state] : ROLLBACK_STATE_UNKNOWN_KEY
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/** mode → 字典键(三选一;其它回落 unknown)。 */
|
|
208
|
+
export function rollbackModeKey(mode: string | null | undefined): string {
|
|
209
|
+
return isRollbackMode(mode) ? MODE_KEYS[mode] : ROLLBACK_MODE_UNKNOWN_KEY
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const GUARD_KEYS: Record<RollbackGuardPolicy, string> = {
|
|
213
|
+
require: 'rollback.guard.require',
|
|
214
|
+
warn: 'rollback.guard.warn',
|
|
215
|
+
off: 'rollback.guard.off',
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/** 救援点策略(S5 guard)→ 字典键(未知回落 unknown)。 */
|
|
219
|
+
export function rollbackGuardPolicyKey(policy: string | null | undefined): string {
|
|
220
|
+
return policy === 'require' || policy === 'warn' || policy === 'off' ? GUARD_KEYS[policy] : 'rollback.guard.unknown'
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/** 机器码 → 字典键(白名单外回落 unknown)。 */
|
|
224
|
+
export function rollbackCodeKey(code: string | null | undefined): string {
|
|
225
|
+
if (typeof code !== 'string' || code === '') return ROLLBACK_CODE_UNKNOWN_KEY
|
|
226
|
+
return ROLLBACK_CODES.includes(code) ? 'rollback.code.' + code : ROLLBACK_CODE_UNKNOWN_KEY
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
export interface RollbackGuidance { code: string; detail?: string }
|
|
230
|
+
export interface RollbackGuidanceRowView { key: string; detail: string | null }
|
|
231
|
+
|
|
232
|
+
/** 指引 / 告警:code → rollback.guidance.* / rollback.warn.*(三种形态都认),detail 过 redact。 */
|
|
233
|
+
export function rollbackGuidanceRows(items: readonly RollbackGuidance[] | undefined): RollbackGuidanceRowView[] {
|
|
234
|
+
return (items ?? []).map((item) => ({
|
|
235
|
+
key: rollbackNoticeKey(item.code),
|
|
236
|
+
detail: item.detail !== undefined && item.detail !== '' ? redact(item.detail) : null,
|
|
237
|
+
}))
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/* ------------------------------------------------------------ 留存口径(S6) */
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* 每会话最多保留的**自动点**(超出只淘汰最旧的自动点 —— 引擎 selectPrune 的自动点桶)。
|
|
244
|
+
* 救援点(guard)不占这个额度、也不被它淘汰,但有自己的**两道**上限
|
|
245
|
+
* (每会话引擎 ROLLBACK_MAX_GUARDS_PER_SESSION / 全局 ROLLBACK_MAX_GUARDS)。
|
|
246
|
+
*/
|
|
247
|
+
export const ROLLBACK_MAX_POINTS_PER_SESSION = 20
|
|
248
|
+
/** 全局最多保留的**自动点**(同上,只淘汰最旧的自动点)。 */
|
|
249
|
+
export const ROLLBACK_MAX_POINTS_TOTAL = 50
|
|
250
|
+
/** 每会话最多保留的**救援点**(guard):超出只淘汰最旧的救援点;数值 = 引擎 ROLLBACK_MAX_GUARDS_PER_SESSION。 */
|
|
251
|
+
export const ROLLBACK_MAX_POINTS_GUARDS_PER_SESSION = 20
|
|
252
|
+
/**
|
|
253
|
+
* **全局**最多保留的**救援点**(guard):与 ROLLBACK_MAX_POINTS_TOTAL **互相独立**
|
|
254
|
+
* (两类各计预算,谁也不吃谁额度;数值相同不是耦合),超出只淘汰最旧的救援点。
|
|
255
|
+
* 数值 = 引擎 ROLLBACK_MAX_GUARDS(两侧必须同步,T17 用例逐句钉住)。
|
|
256
|
+
*/
|
|
257
|
+
export const ROLLBACK_MAX_POINTS_GUARDS_TOTAL = 50
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* 预览不可回退时的原因键(按宿主真实形状分派;**绝不回落 rollback.code.unknown**)。
|
|
261
|
+
*
|
|
262
|
+
* 注意(t22 冻结口径):救援点**可以**作为回退目标 —— 引擎 canRestore = !protected && !incomplete &&
|
|
263
|
+
* 至少一段可恢复(**不含** kind!=='guard',见 src/core/rollback-point.ts:1531)。所以这个键**只在
|
|
264
|
+
* 宿主真的拒绝了一个看起来可恢复的救援点**(canRestore=false 且不是 protected/incomplete/无可用段)时使用,
|
|
265
|
+
* 不是「救援点一律不可回退」。
|
|
266
|
+
*/
|
|
267
|
+
export const ROLLBACK_PREVIEW_REASON_GUARD_KEY = 'rollback.preview.reason.guard'
|
|
268
|
+
export const ROLLBACK_PREVIEW_REASON_INCOMPLETE_KEY = 'rollback.preview.reason.incomplete'
|
|
269
|
+
export const ROLLBACK_PREVIEW_REASON_HOST_REFUSED_KEY = 'rollback.preview.reason.hostRefused'
|
|
270
|
+
|
|
271
|
+
/* ------------------------------------------------------------ 线协议类型(结构子集) */
|
|
272
|
+
|
|
273
|
+
export type RollbackPointKind = 'auto' | 'guard'
|
|
274
|
+
export type RollbackOutcome = 'restored' | 'partial' | 'failed' | 'denied'
|
|
275
|
+
export type RollbackSegmentStatus = 'restored' | 'skipped' | 'failed'
|
|
276
|
+
export type RollbackGuardPolicy = 'require' | 'warn' | 'off'
|
|
277
|
+
|
|
278
|
+
/** 单段摘要(恒为 workspace / session 两项;state 用 string 以便未知值也能被映射成 unknown 键)。 */
|
|
279
|
+
export interface RollbackSegmentSummary {
|
|
280
|
+
state: string
|
|
281
|
+
ok?: boolean
|
|
282
|
+
degraded?: boolean
|
|
283
|
+
capturedAt?: number
|
|
284
|
+
reasonCode?: string | null
|
|
285
|
+
/** 该段捕获的条目数(工作区文件数 / 会话 0|1) */
|
|
286
|
+
entries?: number
|
|
287
|
+
bytes?: number
|
|
288
|
+
/** 会话段:游标(seq) */
|
|
289
|
+
cursorBytes?: number | null
|
|
290
|
+
/** 该段**可回退**(有内容副本 / 有可写回条目) */
|
|
291
|
+
restorable?: boolean
|
|
292
|
+
/** 留点时的**跳过条目总数**(工作区态:skipped + excludedDirs;会话态恒 0)—— S4「跳过并如实报告」 */
|
|
293
|
+
skippedCount?: number
|
|
294
|
+
/** 该段捕获是否被上限截断(工作区态 = 单文件上限 / 字节预算 / 文件数预算任一命中) */
|
|
295
|
+
truncated?: boolean
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
export interface RollbackPointSummary {
|
|
299
|
+
id: string
|
|
300
|
+
createdAt: number
|
|
301
|
+
/** 自动留点归属的会话(自动点必有;无会话的旧记录为 null) */
|
|
302
|
+
sessionId?: string | null
|
|
303
|
+
/** 会话单元 id(宿主回传的 <projectKey>/<sessionId>;用于分组表头) */
|
|
304
|
+
unitId?: string | null
|
|
305
|
+
/**
|
|
306
|
+
* 对话标题:宿主从 DSH 会话缓存补的**只读装饰**(与 /recovery/sessions 同一份索引)。
|
|
307
|
+
* 读不到就**不带该字段** —— 界面回落显示 `sessionId`,绝不编造标题。
|
|
308
|
+
*/
|
|
309
|
+
title?: string | null
|
|
310
|
+
/** 该点对应的回合号(1-based;无法判定为 null) */
|
|
311
|
+
turn?: number | null
|
|
312
|
+
/** fork 的 inclusive 边界 seq(null = 无前置边界) */
|
|
313
|
+
boundarySeq?: number | null
|
|
314
|
+
/** auto = 回合结束自动留点;guard = 回退前救援点 */
|
|
315
|
+
kind?: string
|
|
316
|
+
protected?: boolean
|
|
317
|
+
/** 两态缺一(缺省不允许回退) */
|
|
318
|
+
incomplete?: boolean
|
|
319
|
+
note?: string | null
|
|
320
|
+
/** 两态 capturedAt 的跨度(诚实事实,无同点判定) */
|
|
321
|
+
spreadMs?: number
|
|
322
|
+
segments?: RollbackSegmentSummary[]
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* 台账占用(issue #89 P2:可观测)。
|
|
327
|
+
* `bytes` = records/ + payloads/ 的实际字节数;`budgetBytes` = 触发淘汰的字节预算;
|
|
328
|
+
* `evicted` = 本进程内被预算淘汰的自动点累计数。缺省(老宿主没这个字段)= 界面不渲染这一行。
|
|
329
|
+
*/
|
|
330
|
+
export interface RollbackLedgerStatus {
|
|
331
|
+
records: number
|
|
332
|
+
bytes: number
|
|
333
|
+
budgetBytes: number
|
|
334
|
+
evicted: number
|
|
335
|
+
/**
|
|
336
|
+
* 已超预算且**无处可淘汰**(典型:单条点本身就大于预算)。
|
|
337
|
+
* 老宿主没有这个字段 ⇒ `undefined` 与 `false` 同义,绝不因为读不到就报警。
|
|
338
|
+
*/
|
|
339
|
+
overBudget?: boolean
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
export interface RollbackStorageStatus {
|
|
343
|
+
available: boolean
|
|
344
|
+
root?: string
|
|
345
|
+
detail?: string
|
|
346
|
+
guidance?: RollbackGuidance[]
|
|
347
|
+
requirements?: readonly string[]
|
|
348
|
+
/** 磁盘偏好:回合自动留点开关(缺省 / false = 关闭) */
|
|
349
|
+
rollbackEnabled?: boolean
|
|
350
|
+
/** 本进程此刻是否真的在留点(关闭后订阅已解绑)。与 rollbackEnabled 正常一致 */
|
|
351
|
+
rollbackActive?: boolean
|
|
352
|
+
/** 台账占用(老宿主没有这个字段) */
|
|
353
|
+
ledger?: RollbackLedgerStatus
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
/** 开关读 / 写的结果(POST 成功才会改进程 gate)。 */
|
|
357
|
+
export interface RollbackPrefsResult {
|
|
358
|
+
ok: boolean
|
|
359
|
+
rollbackEnabled: boolean
|
|
360
|
+
rollbackActive?: boolean
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
/** 台账清理结果(只删自动点;guard 与 protected 保留)。 */
|
|
364
|
+
export interface RollbackPurgeResult {
|
|
365
|
+
ok: boolean
|
|
366
|
+
code?: string
|
|
367
|
+
detail?: string
|
|
368
|
+
removedRecords?: number
|
|
369
|
+
removedPayloads?: number
|
|
370
|
+
removedObjects?: number
|
|
371
|
+
bytes?: number
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
export interface RollbackListResult {
|
|
375
|
+
ok?: boolean
|
|
376
|
+
points: RollbackPointSummary[]
|
|
377
|
+
storage: RollbackStorageStatus
|
|
378
|
+
error?: string
|
|
379
|
+
/** 全局留存上限(S6:50) */
|
|
380
|
+
maxRecords?: number
|
|
381
|
+
/** 每会话留存上限(S6:20) */
|
|
382
|
+
maxRecordsPerSession?: number
|
|
383
|
+
/** **全局**救援点留存上限(S6 第二道 guard 预算:50) */
|
|
384
|
+
maxGuards?: number
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
export interface RollbackDetailResult {
|
|
388
|
+
ok: boolean
|
|
389
|
+
id: string
|
|
390
|
+
point: RollbackPointSummary
|
|
391
|
+
storage: RollbackStorageStatus
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
/** 计数或清单:宿主回传数字就取数字,回传清单就取长度(两者都是「有多少项」)。 */
|
|
395
|
+
export type RollbackCount = number | readonly string[]
|
|
396
|
+
|
|
397
|
+
/**
|
|
398
|
+
* 预览里「回退会改动的一个文件」的动作。
|
|
399
|
+
*
|
|
400
|
+
* 与引擎(`src/core/rollback-point.ts` 的 `RollbackPreviewFileStatus`)**逐字同形**:
|
|
401
|
+
* · `overwrite` = 覆盖已有文件;`create` = 当前不存在(回退会重新创建);
|
|
402
|
+
* · `delete` **不由 `files[]` 产出**:整文件删除走**另一条**清单
|
|
403
|
+
* (`RollbackWorkspacePreview.removals` = 会删的,`createdSkipped` = 拒删的)——
|
|
404
|
+
* 那些路径不在该点的记录里,所以既不进这份逐文件统计、也读不出前后对照。
|
|
405
|
+
* 保留该取值只为契约形状向前兼容 —— 界面不得为它编文案。
|
|
406
|
+
*/
|
|
407
|
+
export type RollbackPreviewFileStatus = 'overwrite' | 'create' | 'delete'
|
|
408
|
+
|
|
409
|
+
/**
|
|
410
|
+
* E1:逐文件的行级统计(简洁版预览弹窗 `preview.workspace.files` 的元素形状)。
|
|
411
|
+
*
|
|
412
|
+
* **只列真的会被改动的文件** —— 已与快照一致的条目一个都不列(「353 项里 0 项不同」⇒ 空数组)。
|
|
413
|
+
* `added` / `removed` 可能是 `null`(单文件超上限 / 单张预览累计超限 / Myers 步数超限 /
|
|
414
|
+
* 非文本 / 对象仓读不出),此时 `truncated` 为 true:界面说「行数未统计」,
|
|
415
|
+
* **绝不写成 0**(0 是一个会被证伪的具体数字,而「不知道」不是)。
|
|
416
|
+
*/
|
|
417
|
+
export interface RollbackPreviewFile {
|
|
418
|
+
/** 绝对路径(界面只做展示)。 */
|
|
419
|
+
path: string
|
|
420
|
+
status: RollbackPreviewFileStatus
|
|
421
|
+
/** 回退会让该文件**新增**的行数;null = 本次不给数字(见 truncated)。 */
|
|
422
|
+
added: number | null
|
|
423
|
+
/** 回退会从该文件**删除**的行数;null = 同上。 */
|
|
424
|
+
removed: number | null
|
|
425
|
+
/** true = 本次没有可信行数(added/removed 均为 null)。**宁可说「不知道」,也绝不谎报行数。** */
|
|
426
|
+
truncated: boolean
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
export interface RollbackWorkspacePreview {
|
|
430
|
+
/** 将被写回(覆盖)的条目数 */
|
|
431
|
+
overwrite?: RollbackCount
|
|
432
|
+
/** 与当前内容不同、确实会被改动 */
|
|
433
|
+
different?: RollbackCount
|
|
434
|
+
/** 记录里的文件当前已不存在(回退会重新创建) */
|
|
435
|
+
missing?: RollbackCount
|
|
436
|
+
/**
|
|
437
|
+
* 该点**未记录**的条目总数。**不是**「之后新建」,也**不是**「会被删除」——
|
|
438
|
+
* 会被删的是 {@link removals} / {@link removalsCount}。
|
|
439
|
+
*/
|
|
440
|
+
createdAfter?: RollbackCount
|
|
441
|
+
/** 该点**未记录**的路径清单(含被拒删的;宿主上限 20 条)—— **不是**删除清单 */
|
|
442
|
+
leftovers?: string[]
|
|
443
|
+
leftoversTruncated?: boolean
|
|
444
|
+
/** **会被删除**的路径(= 未记录 + 过了安全判据;宿主上限 20 条) */
|
|
445
|
+
removals?: string[]
|
|
446
|
+
/** 会被删除的**真实总数**(不受清单上限影响) */
|
|
447
|
+
removalsCount?: RollbackCount
|
|
448
|
+
/** 删除逐条清单不完整(条数超上限 / 遍历被截断) */
|
|
449
|
+
removalsTruncated?: boolean
|
|
450
|
+
/** 按安全判据**拒删**的路径(.git / node_modules / 数据区 / 越界 / 过大 / 捕获不完整时的全部候选) */
|
|
451
|
+
createdSkipped?: string[]
|
|
452
|
+
/** 拒删路径的**真实总数** */
|
|
453
|
+
createdSkippedCount?: RollbackCount
|
|
454
|
+
createdSkippedTruncated?: boolean
|
|
455
|
+
/**
|
|
456
|
+
* 非 null = 该点捕获**不完整** ⇒ **一条都没删**(安全拒删:绝不误删既有文件)。
|
|
457
|
+
* 界面必须如实说,绝不把它渲染成「无事可做 / 已一致」。
|
|
458
|
+
*/
|
|
459
|
+
createdRefusedReason?: string | null
|
|
460
|
+
/** 记录在案的分块(绝对路径;界面只做展示,不做判定) */
|
|
461
|
+
chunks?: string[]
|
|
462
|
+
/**
|
|
463
|
+
* E1:逐文件的行级统计 —— **只含会被改动的文件**(内容已一致的条目一个都不列)。
|
|
464
|
+
*
|
|
465
|
+
* 可选(`?`)是**刻意的向后兼容**:老响应 / 老宿主不认识这个字段,缺省必须回落
|
|
466
|
+
* 「没有这条信息」,而不是被当成「空数组=全部一致」或抛错 —— 界面据「字段是否存在」
|
|
467
|
+
* 决定是否显示文件列表,绝不把「缺字段」与「确知为空」混成同一件事。
|
|
468
|
+
*/
|
|
469
|
+
files?: RollbackPreviewFile[]
|
|
470
|
+
/** 捕获时被跳过的条目总数(skipped + excludedDirs;逐条列表的上限截断不影响本计数) */
|
|
471
|
+
skippedCount?: RollbackCount
|
|
472
|
+
/** 捕获是否被上限截断(超单文件上限 / 字节预算 / 文件数预算任一命中即为 true) */
|
|
473
|
+
truncated?: boolean
|
|
474
|
+
available?: boolean
|
|
475
|
+
reasonCode?: string | null
|
|
476
|
+
guidance?: RollbackGuidance[]
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
export interface RollbackSessionPreview {
|
|
480
|
+
/** 回退目标边界:目标回合开始前最后一个事件的 seq(fork 的 inclusive 边界;无边界为 null) */
|
|
481
|
+
from?: number | null
|
|
482
|
+
/** 该回合 turn/end 的 seq(无边界为 null) */
|
|
483
|
+
to?: number | null
|
|
484
|
+
/** 该点对应的回合号 */
|
|
485
|
+
turns?: number
|
|
486
|
+
/** 当前事件游标(seq) */
|
|
487
|
+
cursorSeq?: number | null
|
|
488
|
+
/** 当前日志字节数 */
|
|
489
|
+
cursorNowBytes?: number | null
|
|
490
|
+
/** 目标边界的字节数 */
|
|
491
|
+
cursorTargetBytes?: number | null
|
|
492
|
+
/** 该段是否可回退(无边界 / 副本缺失 → false) */
|
|
493
|
+
restorable?: boolean
|
|
494
|
+
/** 日志刚被写过(仅供参考:fork 只读,不会覆盖日志) */
|
|
495
|
+
active?: boolean
|
|
496
|
+
reasonCode?: string | null
|
|
497
|
+
available?: boolean
|
|
498
|
+
guidance?: RollbackGuidance[]
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
export interface RollbackPreviewResult {
|
|
502
|
+
ok: boolean
|
|
503
|
+
code?: string
|
|
504
|
+
detail?: string
|
|
505
|
+
id: string
|
|
506
|
+
storage: RollbackStorageStatus
|
|
507
|
+
workspace?: RollbackWorkspacePreview | null
|
|
508
|
+
session?: RollbackSessionPreview | null
|
|
509
|
+
/** 该点的两态摘要(宿主回传;缺失时界面用列表行里的同一份摘要) */
|
|
510
|
+
point?: RollbackPointSummary | null
|
|
511
|
+
/** 宿主给出的权威判定(本层在此基础上再要求「至少有一段可回退」) */
|
|
512
|
+
canRestore?: boolean
|
|
513
|
+
canRemove?: boolean
|
|
514
|
+
summary?: RollbackPointSummary
|
|
515
|
+
guidance?: RollbackGuidance[]
|
|
516
|
+
warnings?: RollbackGuidance[]
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
/** 单文件差异里一侧(与引擎 `RollbackFileDiffSide` 逐字同形:字段名/含义都一致)。 */
|
|
520
|
+
export interface RollbackFileDiffSide {
|
|
521
|
+
exists: boolean
|
|
522
|
+
bytes: number
|
|
523
|
+
/** 行数;null = 未读取(二进制 / 超限 / 不可读) */
|
|
524
|
+
lines: number | null
|
|
525
|
+
binary: boolean
|
|
526
|
+
oversized: boolean
|
|
527
|
+
unreadable: boolean
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
/** 无法逐行对比的原因(有值时 hunks 恒为空数组 —— 「不知道」不是「没有差异」)。 */
|
|
531
|
+
export type RollbackFileDiffReason = 'missing-blob' | 'binary' | 'too-large' | 'unreadable'
|
|
532
|
+
|
|
533
|
+
/**
|
|
534
|
+
* 单文件前后对照(引擎 `fileDiff` 的镜像;只列一条记录)。
|
|
535
|
+
*
|
|
536
|
+
* 方向:`before` = **当前磁盘**,`after` = **回退点快照**(点下回退之后该文件的样子)——
|
|
537
|
+
* 与预览行数统计同一口径,界面左右双栏与之一致,别读反。
|
|
538
|
+
*/
|
|
539
|
+
export interface RollbackFileDiffResult {
|
|
540
|
+
ok: boolean
|
|
541
|
+
code?: string
|
|
542
|
+
detail?: string
|
|
543
|
+
id: string
|
|
544
|
+
path: string
|
|
545
|
+
status: RollbackPreviewFileStatus
|
|
546
|
+
before: RollbackFileDiffSide
|
|
547
|
+
after: RollbackFileDiffSide
|
|
548
|
+
hunks: DiffHunk[]
|
|
549
|
+
added: number
|
|
550
|
+
removed: number
|
|
551
|
+
identical: boolean
|
|
552
|
+
degraded: boolean
|
|
553
|
+
reason?: RollbackFileDiffReason
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
export interface RollbackSegmentResult {
|
|
557
|
+
state: string
|
|
558
|
+
status: string
|
|
559
|
+
reasonCode?: string
|
|
560
|
+
/** 逐条事实(技术细节,非用户可见文案) */
|
|
561
|
+
detail?: string[]
|
|
562
|
+
restored?: number
|
|
563
|
+
failed?: number
|
|
564
|
+
/** 该点**未记录**的路径(含被拒删的;宿主上限 20 条)。**不是**删除清单 —— 那一份是 removedPaths */
|
|
565
|
+
leftovers?: string[]
|
|
566
|
+
leftoversTruncated?: boolean
|
|
567
|
+
/** 本次真的删掉的「该点未记录」路径数(用户 2026-10-11 定案 A) */
|
|
568
|
+
removed?: number
|
|
569
|
+
/** 已删除路径(有界清单;与预览的 workspace.removals 同源同序) */
|
|
570
|
+
removedPaths?: string[]
|
|
571
|
+
/** 想删但按安全判据拒删的路径(越界 / 数据区 / .git / node_modules / 过大 / 捕获不完整),如实报告 */
|
|
572
|
+
createdSkipped?: string[]
|
|
573
|
+
/** 拒删路径的**真实总数**(不受清单上限影响) */
|
|
574
|
+
createdSkippedCount?: RollbackCount
|
|
575
|
+
createdSkippedTruncated?: boolean
|
|
576
|
+
/** 非 null = 该点捕获**不完整** ⇒ 一条都没删(安全拒删) */
|
|
577
|
+
createdRefusedReason?: string | null
|
|
578
|
+
/** 工作区段:实际写回的字节数(宿主回传才有) */
|
|
579
|
+
writtenBytes?: number
|
|
580
|
+
/** 兼容形态:宿主只回传计数时也算得出来 */
|
|
581
|
+
createdAfter?: RollbackCount
|
|
582
|
+
createdAfterTruncated?: boolean
|
|
583
|
+
/** 会话段:回退产生的 fork 子会话 id */
|
|
584
|
+
childSessionId?: string
|
|
585
|
+
guidance?: RollbackGuidance[]
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
export interface RollbackRewindResult {
|
|
589
|
+
ok: boolean
|
|
590
|
+
outcome: string
|
|
591
|
+
code: string
|
|
592
|
+
detail?: string
|
|
593
|
+
id?: string
|
|
594
|
+
segments?: RollbackSegmentResult[]
|
|
595
|
+
/** 因前段失败 / 未请求而未执行的段(只可能是两态) */
|
|
596
|
+
skippedSegments?: string[]
|
|
597
|
+
/** 会话回退结果:fork 出来的子会话 id(S3;原会话日志只读,绝不改写) */
|
|
598
|
+
childSessionId?: string
|
|
599
|
+
/** 回退前自动留的救援点 id(S5 guard) */
|
|
600
|
+
guardPointId?: string
|
|
601
|
+
storage?: RollbackStorageStatus
|
|
602
|
+
guidance?: RollbackGuidance[]
|
|
603
|
+
warnings?: RollbackGuidance[]
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
export interface RollbackRemoveResult { ok: boolean; code: string; detail?: string; id: string }
|
|
607
|
+
|
|
608
|
+
/** 回退输入(冻结 wire contract 的 rewind body)。 */
|
|
609
|
+
export interface RollbackRewindOptions {
|
|
610
|
+
/** fail-closed 确认门:必须**恰好** true(宿主缺省/假值一律 409 confirmation-required) */
|
|
611
|
+
userConfirmed: boolean
|
|
612
|
+
mode?: RollbackMode
|
|
613
|
+
segments?: readonly RollbackState[]
|
|
614
|
+
allowPartial?: boolean
|
|
615
|
+
preRewindGuard?: RollbackGuardPolicy
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
/** 端口:由 src/client/recovery/recovery-api.ts 实现(Partial 消费,缺失时界面如实说能力缺失)。 */
|
|
619
|
+
export interface RollbackPointPort {
|
|
620
|
+
rollbackPoints(): Promise<RollbackListResult>
|
|
621
|
+
rollbackPointStorage(): Promise<RollbackStorageStatus>
|
|
622
|
+
rollbackPoint(id: string): Promise<RollbackDetailResult>
|
|
623
|
+
rollbackPointPreview(id: string): Promise<RollbackPreviewResult>
|
|
624
|
+
/**
|
|
625
|
+
* 只读:该点里**一个记录在案文件**的前后逐行对照(预览弹窗按需拉取;未命中记录一律拒绝)。
|
|
626
|
+
* 可选实现:端口缺失该方法时,界面**不显示**「查看」(绝不给一个点了没反应的入口)。
|
|
627
|
+
*/
|
|
628
|
+
rollbackPointFileDiff?(id: string, path: string): Promise<RollbackFileDiffResult>
|
|
629
|
+
restoreRollbackPoint(id: string, opts: RollbackRewindOptions): Promise<RollbackRewindResult>
|
|
630
|
+
removeRollbackPoint(id: string, userConfirmed: boolean): Promise<RollbackRemoveResult>
|
|
631
|
+
/**
|
|
632
|
+
* 回合自动留点开关(issue #88:它跟配置备份/同步是两件事,必须能关)。
|
|
633
|
+
* 可选实现:端口缺失时界面**不渲染**开关(绝不给一个点了没反应的入口)。
|
|
634
|
+
*/
|
|
635
|
+
rollbackPrefs?(): Promise<RollbackPrefsResult>
|
|
636
|
+
setRollbackEnabled?(enabled: boolean): Promise<RollbackPrefsResult>
|
|
637
|
+
/**
|
|
638
|
+
* 清理台账(issue #89 P1/P2:把已经涨上去的占用收回来)。
|
|
639
|
+
* **只删自动点**;`includeGuards` 为 true 时连 guard 也删,protected 永不删。
|
|
640
|
+
*/
|
|
641
|
+
purgeRollbackPoints?(opts: { includeGuards: boolean }): Promise<RollbackPurgeResult>
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
/**
|
|
645
|
+
* 聊天内「↺ 回退」入口是否可见(issue #88:关闭后**收起**入口,不是置灰)。
|
|
646
|
+
*
|
|
647
|
+
* 判据只有一条:**宿主回传的开关**。老宿主(storage 响应里没有 `rollbackEnabled` 字段)按
|
|
648
|
+
* 「未提供 = 功能默认开」处理 —— 那一版的语义就是默认启用,此时把入口藏起来会让用户
|
|
649
|
+
* 「有点但没有入口」。形状不符(不是布尔)同样按「未提供」处理,绝不因为一个坏字段藏掉入口。
|
|
650
|
+
*/
|
|
651
|
+
export function rollbackEntryVisible(storage: RollbackStorageStatus | null | undefined): boolean {
|
|
652
|
+
if (storage === undefined || storage === null) return false
|
|
653
|
+
return storage.rollbackEnabled !== false
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
/* ------------------------------------------------------------ 内部小工具 */
|
|
657
|
+
|
|
658
|
+
function countOf(value: RollbackCount | undefined): number {
|
|
659
|
+
if (typeof value === 'number' && Number.isFinite(value)) return value
|
|
660
|
+
if (Array.isArray(value)) return value.length
|
|
661
|
+
return 0
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
function numberOrNull(value: number | null | undefined): number | null {
|
|
665
|
+
return typeof value === 'number' && Number.isFinite(value) ? value : null
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
/** 路径/事实清单:逐条脱敏(宿主技术细节不得明文上屏)。 */
|
|
669
|
+
function redactLines(values: readonly string[] | undefined): string[] {
|
|
670
|
+
return (values ?? []).map((line) => redact(line))
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
/** 段可用性:显式 available/restorable 为 false 才算不可用(缺字段 = 宿主没给否定信号)。 */
|
|
674
|
+
function segmentAvailable(segment: { available?: boolean; restorable?: boolean }): boolean {
|
|
675
|
+
return segment.available !== false && segment.restorable !== false
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
/* ------------------------------------------------------------ 列表 → 展示行 */
|
|
679
|
+
|
|
680
|
+
export interface RollbackSegmentView {
|
|
681
|
+
state: string
|
|
682
|
+
labelKey: string
|
|
683
|
+
restorable: boolean
|
|
684
|
+
/** ok = 正常;warn = 捕获不完整(degraded);error = 没捕到 / 失败 */
|
|
685
|
+
badgeKind: 'ok' | 'warn' | 'error'
|
|
686
|
+
entries: number
|
|
687
|
+
cursorBytes: number | null
|
|
688
|
+
reasonKey: string | null
|
|
689
|
+
/** 留点时被跳过的条目总数(S4:跳过必须可见,绝不静默) */
|
|
690
|
+
skippedCount: number
|
|
691
|
+
/** 该段捕获被上限截断 */
|
|
692
|
+
truncated: boolean
|
|
693
|
+
/** 该段要显示的告警键(有序;空数组 = 没有需要说的)—— 文案在字典里,本层只回键 */
|
|
694
|
+
noticeKeys: string[]
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
/** 段级 / 点级「跳过 · 截断」告警键(S4:宿主已如实回传,界面必须说出来)。 */
|
|
698
|
+
export const ROLLBACK_NOTICE_SKIPPED_KEY = 'rollback.row.notice.skipped'
|
|
699
|
+
export const ROLLBACK_NOTICE_TRUNCATED_KEY = 'rollback.row.notice.truncated'
|
|
700
|
+
export const ROLLBACK_PREVIEW_NOTICE_SKIPPED_KEY = 'rollback.preview.notice.skipped'
|
|
701
|
+
export const ROLLBACK_PREVIEW_NOTICE_TRUNCATED_KEY = 'rollback.preview.notice.truncated'
|
|
702
|
+
|
|
703
|
+
/** 跳过 / 截断事实 → 告警键(顺序固定:先「跳过多少」,再「被截断」)。 */
|
|
704
|
+
function noticeKeysOf(skippedCount: number, truncated: boolean, skippedKey: string, truncatedKey: string): string[] {
|
|
705
|
+
const keys: string[] = []
|
|
706
|
+
if (skippedCount > 0) keys.push(skippedKey)
|
|
707
|
+
if (truncated) keys.push(truncatedKey)
|
|
708
|
+
return keys
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
export interface RollbackPointRowView {
|
|
712
|
+
id: string
|
|
713
|
+
/** 来源:auto(回合结束自动留点)/ guard(回退前救援点)/ unknown */
|
|
714
|
+
kind: RollbackPointKind | 'unknown'
|
|
715
|
+
kindKey: string
|
|
716
|
+
createdAtMs: number
|
|
717
|
+
sessionId: string | null
|
|
718
|
+
/** 会话单元 id(宿主回传才有;分组表头显示用) */
|
|
719
|
+
unitId: string | null
|
|
720
|
+
/** 对话标题(宿主补的只读装饰,**渲染前已过 redact**;null = 缓存里没有,界面回落显示 sessionId) */
|
|
721
|
+
title: string | null
|
|
722
|
+
/** 该点对应的回合号(null = 未知,界面渲染 rollback.turnUnknown) */
|
|
723
|
+
turn: number | null
|
|
724
|
+
protected: boolean
|
|
725
|
+
incomplete: boolean
|
|
726
|
+
badgeKind: 'ok' | 'warn' | 'error'
|
|
727
|
+
spreadMs: number
|
|
728
|
+
segments: RollbackSegmentView[]
|
|
729
|
+
/**
|
|
730
|
+
* 至少有一段可回退、未保护、且两态齐全 —— 界面据此决定「回退」按钮可用性。
|
|
731
|
+
* **救援点可以为 true**(t22:回退准入只拒 protected / incomplete;救援点正是「回到回退前」的唯一路径)。
|
|
732
|
+
*/
|
|
733
|
+
rewindable: boolean
|
|
734
|
+
/**
|
|
735
|
+
* 可删:受保护点与**救援点**都不可删(宿主 remove 一律 protected-point)。
|
|
736
|
+
* 与 rewindable **互相独立**(引擎 canRemove / canRestore 两门,src/core/rollback-point.ts:1530-1531)。
|
|
737
|
+
*/
|
|
738
|
+
removable: boolean
|
|
739
|
+
/** 用户自由文本:**渲染前已过 redact** */
|
|
740
|
+
note: string | null
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
function segmentView(segment: RollbackSegmentSummary): RollbackSegmentView {
|
|
744
|
+
const ok = segment.ok !== false
|
|
745
|
+
const degraded = segment.degraded === true
|
|
746
|
+
const badgeKind: 'ok' | 'warn' | 'error' = ok ? (degraded ? 'warn' : 'ok') : 'error'
|
|
747
|
+
const skippedCount = numberOrNull(segment.skippedCount) ?? 0
|
|
748
|
+
const truncated = segment.truncated === true
|
|
749
|
+
return {
|
|
750
|
+
state: segment.state,
|
|
751
|
+
labelKey: rollbackStateKey(segment.state),
|
|
752
|
+
restorable: segment.restorable === true,
|
|
753
|
+
badgeKind,
|
|
754
|
+
entries: numberOrNull(segment.entries) ?? 0,
|
|
755
|
+
cursorBytes: numberOrNull(segment.cursorBytes),
|
|
756
|
+
reasonKey: ok ? null : rollbackCodeKey(segment.reasonCode),
|
|
757
|
+
skippedCount,
|
|
758
|
+
truncated,
|
|
759
|
+
noticeKeys: noticeKeysOf(skippedCount, truncated, ROLLBACK_NOTICE_SKIPPED_KEY, ROLLBACK_NOTICE_TRUNCATED_KEY),
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
/** 列表 → 展示行(台账已按时间倒序;本函数只做映射与判定,不重排)。 */
|
|
764
|
+
export function rollbackPointRows(list: RollbackListResult | null): RollbackPointRowView[] {
|
|
765
|
+
if (list === null) return []
|
|
766
|
+
const points = Array.isArray(list.points) ? list.points : []
|
|
767
|
+
return points.map((point) => {
|
|
768
|
+
const segments = (Array.isArray(point.segments) ? point.segments : []).map(segmentView)
|
|
769
|
+
const kind: RollbackPointKind | 'unknown' = point.kind === 'guard' ? 'guard' : point.kind === 'auto' ? 'auto' : 'unknown'
|
|
770
|
+
const kindKey = kind === 'guard' ? 'rollback.kind.guard' : kind === 'auto' ? 'rollback.kind.auto' : ROLLBACK_KIND_UNKNOWN_KEY
|
|
771
|
+
return {
|
|
772
|
+
id: point.id,
|
|
773
|
+
kind,
|
|
774
|
+
kindKey,
|
|
775
|
+
createdAtMs: point.createdAt,
|
|
776
|
+
sessionId: typeof point.sessionId === 'string' && point.sessionId !== '' ? point.sessionId : null,
|
|
777
|
+
unitId: typeof point.unitId === 'string' && point.unitId !== '' ? point.unitId : null,
|
|
778
|
+
title: typeof point.title === 'string' && point.title.trim() !== '' ? redact(point.title) : null,
|
|
779
|
+
turn: numberOrNull(point.turn),
|
|
780
|
+
protected: point.protected === true,
|
|
781
|
+
incomplete: point.incomplete === true,
|
|
782
|
+
badgeKind: point.incomplete === true ? 'warn' : 'ok',
|
|
783
|
+
spreadMs: numberOrNull(point.spreadMs) ?? 0,
|
|
784
|
+
segments,
|
|
785
|
+
rewindable: point.protected !== true && point.incomplete !== true && segments.some((segment) => segment.restorable),
|
|
786
|
+
// 宿主真值(引擎 preview/remove 同一判据):protected 与 **guard** 一律不可删 ——
|
|
787
|
+
// 只看 protected 会把救援点的「删除」按钮点亮,点了必得 protected-point(按钮与文案自相矛盾)。
|
|
788
|
+
removable: point.protected !== true && kind !== 'guard',
|
|
789
|
+
note: point.note !== null && point.note !== undefined && point.note !== '' ? redact(point.note) : null,
|
|
790
|
+
}
|
|
791
|
+
})
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
/* ------------------------------------------------------------ 按会话分组(时间线) */
|
|
795
|
+
|
|
796
|
+
export interface RollbackPointGroupView {
|
|
797
|
+
/** 会话 id(宿主未回传则为 null —— 界面渲染 rollback.group.unknownSession,绝不猜归属) */
|
|
798
|
+
sessionId: string | null
|
|
799
|
+
/** 会话单元 id(宿主回传才有) */
|
|
800
|
+
unitId: string | null
|
|
801
|
+
/** 对话标题(组内任一记录带标题即取之;null = 无标题,界面回落显示 sessionId) */
|
|
802
|
+
title: string | null
|
|
803
|
+
/** 组内最新时间(空组为 null;组间排序键) */
|
|
804
|
+
latestAtMs: number | null
|
|
805
|
+
rows: RollbackPointRowView[]
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
/**
|
|
809
|
+
* 台账 → **按会话分组**的时间线(组内保持台账顺序 = 时间倒序;组间按各组最新时间倒序)。
|
|
810
|
+
*
|
|
811
|
+
* 分组键 = sessionId;缺 sessionId 的记录归入「未知会话」组并排在最后 —— 绝不猜它属于哪个会话。
|
|
812
|
+
*/
|
|
813
|
+
export function rollbackPointGroups(list: RollbackListResult | null): RollbackPointGroupView[] {
|
|
814
|
+
const groups: RollbackPointGroupView[] = []
|
|
815
|
+
const index = new Map<string, number>()
|
|
816
|
+
for (const row of rollbackPointRows(list)) {
|
|
817
|
+
const key = row.sessionId ?? '\u0000unknown'
|
|
818
|
+
let at = index.get(key)
|
|
819
|
+
if (at === undefined) {
|
|
820
|
+
at = groups.length
|
|
821
|
+
index.set(key, at)
|
|
822
|
+
groups.push({ sessionId: row.sessionId, unitId: row.unitId, title: row.title, latestAtMs: null, rows: [] })
|
|
823
|
+
}
|
|
824
|
+
const group = groups[at] as RollbackPointGroupView
|
|
825
|
+
group.rows.push(row)
|
|
826
|
+
if (group.unitId === null && row.unitId !== null) group.unitId = row.unitId
|
|
827
|
+
if (group.title === null && row.title !== null) group.title = row.title
|
|
828
|
+
if (group.latestAtMs === null || row.createdAtMs > group.latestAtMs) group.latestAtMs = row.createdAtMs
|
|
829
|
+
}
|
|
830
|
+
return groups.sort((a, b) => {
|
|
831
|
+
if (a.sessionId === null && b.sessionId !== null) return 1
|
|
832
|
+
if (b.sessionId === null && a.sessionId !== null) return -1
|
|
833
|
+
return (b.latestAtMs ?? 0) - (a.latestAtMs ?? 0)
|
|
834
|
+
})
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
/** 该回滚点记下了多少个工作区文件(时间线上的「文件数」)。 */
|
|
838
|
+
export function rollbackRowFileCount(row: RollbackPointRowView): number {
|
|
839
|
+
let total = 0
|
|
840
|
+
for (const segment of row.segments) if (segment.state === 'workspace') total += segment.entries
|
|
841
|
+
return total
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
/** 该回滚点是否有会话游标(时间线上的「是否有会话游标」—— 没有就只回退工作区)。 */
|
|
845
|
+
export function rollbackRowHasSessionCursor(row: RollbackPointRowView): boolean {
|
|
846
|
+
return row.segments.some((segment) => segment.state === 'session' && segment.cursorBytes !== null)
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
/* ------------------------------------------------------------ 预览 → 展示行 */
|
|
850
|
+
|
|
851
|
+
export interface RollbackCountRowView { key: string; n: number }
|
|
852
|
+
|
|
853
|
+
export interface RollbackPreviewRowView {
|
|
854
|
+
state: string
|
|
855
|
+
labelKey: string
|
|
856
|
+
available: boolean
|
|
857
|
+
/** 宿主给出的原因码键(null = 没给;不可用时至少回落到 unknown,绝不裸渲染) */
|
|
858
|
+
reasonKey: string | null
|
|
859
|
+
/** 有序计数文案:overwrite / different / missing / createdAfter(工作区段才有计数) */
|
|
860
|
+
counts: RollbackCountRowView[]
|
|
861
|
+
/** 该点**未记录**的路径清单(含被拒删的;已脱敏;宿主上限 20 条)—— **不是**删除清单 */
|
|
862
|
+
leftovers: string[]
|
|
863
|
+
leftoversTruncated: boolean
|
|
864
|
+
/**
|
|
865
|
+
* **会被删除**的路径(已脱敏)—— **全量**(引擎不再切 20 条),与执行阶段同一份规划 ⇒ 逐条一致。
|
|
866
|
+
* 界面直接把它渲染成「删除」行,**不需要**任何「清单可能不完整」的说明句。
|
|
867
|
+
*/
|
|
868
|
+
removals: string[]
|
|
869
|
+
/** 会被删除的条数(= {@link removals}.length) */
|
|
870
|
+
removalsCount: number
|
|
871
|
+
removalsTruncated: boolean
|
|
872
|
+
/**
|
|
873
|
+
* 按安全判据**拒删**的路径与条数、以及「**证明名单不完整** ⇒ 一条都没删」的原因。
|
|
874
|
+
* **正文不渲染**(用户 2026-10-12:弹窗只要清单,不要说明句;文案克制 → DESIGN.md §7)——
|
|
875
|
+
* 字段保留是引擎事实的投影,将来若放进 ⓘ / 工具提示可直接用。
|
|
876
|
+
*/
|
|
877
|
+
createdSkipped: string[]
|
|
878
|
+
createdSkippedCount: number
|
|
879
|
+
createdSkippedTruncated: boolean
|
|
880
|
+
createdRefusedReason: string | null
|
|
881
|
+
/** 留点时被跳过的条目总数(S4:跳过必须可见) */
|
|
882
|
+
skippedCount: number
|
|
883
|
+
/** 捕获被上限截断 */
|
|
884
|
+
truncated: boolean
|
|
885
|
+
/** 该行要显示的「跳过 · 截断」告警键(有序;空数组 = 没有要说的) */
|
|
886
|
+
noticeKeys: string[]
|
|
887
|
+
/**
|
|
888
|
+
* **点级**告警(宿主 preview.warnings;走 rollback.warn.* 映射、未知码回落 unknown)。
|
|
889
|
+
*
|
|
890
|
+
* 引擎不给「告警属于哪一段」的键(detail 是自由文本,解析它等于猜),所以点级告警**两行都带同一份**,
|
|
891
|
+
* 由界面在面板级渲染一次(rollbackPreviewWarnings())—— 本层绝不猜归属。
|
|
892
|
+
*/
|
|
893
|
+
warnings: RollbackGuidanceRowView[]
|
|
894
|
+
/** 会话段:边界游标 from → to(seq;缺失为 null) */
|
|
895
|
+
cursor: { from: number | null; to: number | null } | null
|
|
896
|
+
/** 会话段:日志字节数 now → target(宿主给得出才有;界面据此显示「会话 now→target」) */
|
|
897
|
+
cursorBytes: { now: number | null; target: number | null } | null
|
|
898
|
+
/** 会话段:该点对应的回合号(null = 未知,界面渲染 rollback.turnUnknown 口径) */
|
|
899
|
+
turns: number | null
|
|
900
|
+
/** 会话段:日志刚被写过(仅供参考:fork 只读,不会覆盖日志) */
|
|
901
|
+
active: boolean
|
|
902
|
+
guidance: RollbackGuidanceRowView[]
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
/**
|
|
906
|
+
* 预览 → 展示行(<id>/preview 的只读结果)。
|
|
907
|
+
*
|
|
908
|
+
* 计数口径逐条对应「界面能看到什么」:覆盖多少项、其中多少项内容不同、多少项当前已不存在
|
|
909
|
+
* (会被重新创建)、多少项是该点**未记录**的(引擎只统计「不在记录里」的文件,留点时被跳过的
|
|
910
|
+
* 既有文件同样落在这里,所以不得声称「之后新建」)。
|
|
911
|
+
*
|
|
912
|
+
* 「未记录」与「会被删除」是**两件事**:会被删的只有过了安全判据的那一份
|
|
913
|
+
* (`workspace.removals` / `removalsCount`),未记录清单里混着永不删除的 `.git` 等拒删项。
|
|
914
|
+
*/
|
|
915
|
+
/**
|
|
916
|
+
* 文件行的**主标签** = 文件名(Cursor「Confirm Undo」式:一眼看是哪几个文件,而不是一屏绝对路径)。
|
|
917
|
+
* 完整路径由组件放进 `title`(悬停可见),**信息一个都不丢**。
|
|
918
|
+
*/
|
|
919
|
+
export function rollbackPreviewFileLabel(filePath: string): string {
|
|
920
|
+
const trimmed = filePath.replace(/[\\/]+$/, '')
|
|
921
|
+
const at = Math.max(trimmed.lastIndexOf('/'), trimmed.lastIndexOf('\\'))
|
|
922
|
+
return at < 0 ? trimmed : trimmed.slice(at + 1)
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
/**
|
|
926
|
+
* 文件行的**目录前缀**(弱化色那一段)+ 基名。
|
|
927
|
+
*
|
|
928
|
+
* 为什么必须有它(2026-10-12 用户截图实测):只显示基名时 `.git/index` / `.git/HEAD` /
|
|
929
|
+
* `.git/COMMIT_EDITMSG` 在屏幕上就是三个孤零零的英文单词,用户第一眼读不出「这是什么、在哪」;
|
|
930
|
+
* 而完整路径又太长会把 ±行数推走。折中 = **目录弱化 + 基名主色 + 完整路径仍在 title**(信息不丢)。
|
|
931
|
+
*
|
|
932
|
+
* 语义(两边界都是刻意的):
|
|
933
|
+
* · 路径里没有分隔符(`AGENTS.md` 也在根目录)⇒ `dir === ''` —— 调用方**不画**那一段,
|
|
934
|
+
* 不会出现一颗孤零零的 `/`;
|
|
935
|
+
* · 尾分隔符一律剥掉(`src/a/` ⇒ dir `src`、base `a`),与 rollbackPreviewFileLabel 同一口径;
|
|
936
|
+
* · 只在**最后一个**分隔符处切(Windows 盘符 `C:\\a\\b` ⇒ dir `C:\\a`)。
|
|
937
|
+
*/
|
|
938
|
+
export interface RollbackPathParts {
|
|
939
|
+
/** 目录(含尾随分隔符);根目录文件是空串 */
|
|
940
|
+
dir: string
|
|
941
|
+
/** 基名 */
|
|
942
|
+
base: string
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
export function splitRollbackPath(filePath: string): RollbackPathParts {
|
|
946
|
+
const trimmed = filePath.replace(/[\\/]+$/, '')
|
|
947
|
+
const at = Math.max(trimmed.lastIndexOf('/'), trimmed.lastIndexOf('\\'))
|
|
948
|
+
if (at < 0) return { dir: '', base: trimmed }
|
|
949
|
+
return { dir: trimmed.slice(0, at + 1), base: trimmed.slice(at + 1) }
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
/**
|
|
953
|
+
* 预览**汇总**(弹窗顶部那一条):这次回退到底动多少东西。
|
|
954
|
+
*
|
|
955
|
+
* 为什么这是这一屏最该先看的信息:用户点「↺ 回退」后来到弹窗,先要判断「规模」再逐行核文件;
|
|
956
|
+
* 没有汇总就得爬到第 8 行才知道「原来是 8 个文件」。所以它不是装饰,是**决策信息**
|
|
957
|
+
* (DESIGN §7:只留能改变决定的信息 —— 数字留,描述性句子不留)。
|
|
958
|
+
*
|
|
959
|
+
* 口径三条(逐条可证伪,界面不得超出这里说的):
|
|
960
|
+
* · `files` = **会被改动**的文件数(= 传进来的行数本身,不减去任何东西);
|
|
961
|
+
* · `added` / `removed` **只累加可信行数**:任一侧是 null(超限 / 非文本 / 读不出)的文件两列都不计,
|
|
962
|
+
* 否则「行数未统计」的文件会被当成 0 行 —— 那是一个会被证伪的具体断言(见 ChatPreviewFileRow 注释);
|
|
963
|
+
* · 删除集是另一条清单(`removals`,与执行阶段同一份规划),**单独计数**,不与上方混加。
|
|
964
|
+
*/
|
|
965
|
+
export interface RollbackPreviewScope {
|
|
966
|
+
files: number
|
|
967
|
+
/** 新增行数合计(只含两侧都可信的行) */
|
|
968
|
+
added: number
|
|
969
|
+
/** 删除行数合计(同上) */
|
|
970
|
+
removed: number
|
|
971
|
+
/** 整文件删除条数(引擎 removals,真的会删) */
|
|
972
|
+
removals: number
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
export function rollbackPreviewScope(
|
|
976
|
+
files: readonly { added: number | null; removed: number | null }[],
|
|
977
|
+
removals: readonly unknown[],
|
|
978
|
+
): RollbackPreviewScope {
|
|
979
|
+
let added = 0
|
|
980
|
+
let removed = 0
|
|
981
|
+
for (const file of files) {
|
|
982
|
+
if (file.added === null || file.removed === null) continue
|
|
983
|
+
added += file.added
|
|
984
|
+
removed += file.removed
|
|
985
|
+
}
|
|
986
|
+
return { files: files.length, added, removed, removals: removals.length }
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
/**
|
|
990
|
+
* 差异无法逐行展示时的字典键(本层**只回键**,文案在字典里)。
|
|
991
|
+
* 未知 / 缺失原因回落 null —— 调用方按「未知原因」出话,绝不回落成「没有差异」(那是另一件事)。
|
|
992
|
+
*/
|
|
993
|
+
export function rollbackFileDiffReasonKey(reason: string | null | undefined): string | null {
|
|
994
|
+
switch (reason) {
|
|
995
|
+
case 'missing-blob': return 'rollback.diff.reason.missingBlob'
|
|
996
|
+
case 'binary': return 'rollback.diff.reason.binary'
|
|
997
|
+
case 'too-large': return 'rollback.diff.reason.tooLarge'
|
|
998
|
+
case 'unreadable': return 'rollback.diff.reason.unreadable'
|
|
999
|
+
default: return null
|
|
1000
|
+
}
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
/** 可渲染的 hunks(缺字段 / 非数组一律当空 —— 渲染层绝不因宿主形态差异崩掉)。 */
|
|
1004
|
+
export function rollbackFileDiffHunks(result: RollbackFileDiffResult | null): DiffHunk[] {
|
|
1005
|
+
const hunks = result?.hunks
|
|
1006
|
+
return Array.isArray(hunks) ? hunks : []
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1009
|
+
/** 两侧是否都真的读到了文本(否则说不出「无差异」)。 */
|
|
1010
|
+
export function rollbackFileDiffComparable(result: RollbackFileDiffResult | null): boolean {
|
|
1011
|
+
if (result === null || result.ok !== true) return false
|
|
1012
|
+
if (result.reason !== undefined) return false
|
|
1013
|
+
return result.before.binary !== true && result.after.binary !== true && result.before.oversized !== true && result.after.oversized !== true
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
export function rollbackPreviewRows(preview: RollbackPreviewResult | null): RollbackPreviewRowView[] {
|
|
1017
|
+
if (preview === null) return []
|
|
1018
|
+
const rows: RollbackPreviewRowView[] = []
|
|
1019
|
+
const pointWarnings = rollbackGuidanceRows(preview.warnings)
|
|
1020
|
+
const workspace = preview.workspace
|
|
1021
|
+
if (workspace !== undefined && workspace !== null) {
|
|
1022
|
+
const skippedCount = countOf(workspace.skippedCount)
|
|
1023
|
+
const truncated = workspace.truncated === true
|
|
1024
|
+
rows.push({
|
|
1025
|
+
state: 'workspace',
|
|
1026
|
+
labelKey: rollbackStateKey('workspace'),
|
|
1027
|
+
available: segmentAvailable(workspace),
|
|
1028
|
+
reasonKey: workspace.reasonCode !== undefined && workspace.reasonCode !== null && workspace.reasonCode !== ''
|
|
1029
|
+
? rollbackCodeKey(workspace.reasonCode)
|
|
1030
|
+
: null,
|
|
1031
|
+
counts: [
|
|
1032
|
+
{ key: 'rollback.preview.overwrite', n: countOf(workspace.overwrite) },
|
|
1033
|
+
{ key: 'rollback.preview.different', n: countOf(workspace.different) },
|
|
1034
|
+
{ key: 'rollback.preview.missing', n: countOf(workspace.missing) },
|
|
1035
|
+
{ key: 'rollback.preview.createdAfter', n: Math.max(countOf(workspace.createdAfter), countOf(workspace.leftovers)) },
|
|
1036
|
+
],
|
|
1037
|
+
leftovers: redactLines(workspace.leftovers),
|
|
1038
|
+
leftoversTruncated: workspace.leftoversTruncated === true,
|
|
1039
|
+
removals: redactLines(workspace.removals),
|
|
1040
|
+
removalsCount: Math.max(countOf(workspace.removalsCount), Array.isArray(workspace.removals) ? workspace.removals.length : 0),
|
|
1041
|
+
removalsTruncated: workspace.removalsTruncated === true,
|
|
1042
|
+
createdSkipped: redactLines(workspace.createdSkipped),
|
|
1043
|
+
createdSkippedCount: Math.max(countOf(workspace.createdSkippedCount), Array.isArray(workspace.createdSkipped) ? workspace.createdSkipped.length : 0),
|
|
1044
|
+
createdSkippedTruncated: workspace.createdSkippedTruncated === true,
|
|
1045
|
+
createdRefusedReason: typeof workspace.createdRefusedReason === 'string' && workspace.createdRefusedReason !== '' ? workspace.createdRefusedReason : null,
|
|
1046
|
+
skippedCount,
|
|
1047
|
+
truncated,
|
|
1048
|
+
noticeKeys: noticeKeysOf(skippedCount, truncated, ROLLBACK_PREVIEW_NOTICE_SKIPPED_KEY, ROLLBACK_PREVIEW_NOTICE_TRUNCATED_KEY),
|
|
1049
|
+
warnings: pointWarnings,
|
|
1050
|
+
cursor: null,
|
|
1051
|
+
cursorBytes: null,
|
|
1052
|
+
turns: null,
|
|
1053
|
+
active: false,
|
|
1054
|
+
guidance: rollbackGuidanceRows(workspace.guidance),
|
|
1055
|
+
})
|
|
1056
|
+
}
|
|
1057
|
+
const session = preview.session
|
|
1058
|
+
if (session !== undefined && session !== null) {
|
|
1059
|
+
rows.push({
|
|
1060
|
+
state: 'session',
|
|
1061
|
+
labelKey: rollbackStateKey('session'),
|
|
1062
|
+
available: segmentAvailable(session),
|
|
1063
|
+
reasonKey: session.reasonCode !== undefined && session.reasonCode !== null && session.reasonCode !== ''
|
|
1064
|
+
? rollbackCodeKey(session.reasonCode)
|
|
1065
|
+
: null,
|
|
1066
|
+
counts: [],
|
|
1067
|
+
leftovers: [],
|
|
1068
|
+
leftoversTruncated: false,
|
|
1069
|
+
removals: [],
|
|
1070
|
+
removalsCount: 0,
|
|
1071
|
+
removalsTruncated: false,
|
|
1072
|
+
createdSkipped: [],
|
|
1073
|
+
createdSkippedCount: 0,
|
|
1074
|
+
createdSkippedTruncated: false,
|
|
1075
|
+
createdRefusedReason: null,
|
|
1076
|
+
skippedCount: 0,
|
|
1077
|
+
truncated: false,
|
|
1078
|
+
noticeKeys: [],
|
|
1079
|
+
warnings: pointWarnings,
|
|
1080
|
+
cursor: { from: numberOrNull(session.from), to: numberOrNull(session.to) },
|
|
1081
|
+
cursorBytes: { now: numberOrNull(session.cursorNowBytes), target: numberOrNull(session.cursorTargetBytes) },
|
|
1082
|
+
turns: numberOrNull(session.turns),
|
|
1083
|
+
active: session.active === true,
|
|
1084
|
+
guidance: rollbackGuidanceRows(session.guidance),
|
|
1085
|
+
})
|
|
1086
|
+
}
|
|
1087
|
+
return rows
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
export interface RollbackPreviewVerdict {
|
|
1091
|
+
/** 宿主允许回退 **且** 至少有一段可回退 */
|
|
1092
|
+
canRestore: boolean
|
|
1093
|
+
canRemove: boolean
|
|
1094
|
+
/** canRestore=false 时的原因键(null = 可回退) */
|
|
1095
|
+
reasonKey: string | null
|
|
1096
|
+
/** 三选一 mode(按可用段推导,界面据此作为下拉默认值) */
|
|
1097
|
+
mode: RollbackMode
|
|
1098
|
+
/** 可回退的段(只可能是 workspace / session) */
|
|
1099
|
+
segments: RollbackState[]
|
|
1100
|
+
/** 该点两态不全(缺省不允许回退;界面据此要求显式勾「允许部分回退」) */
|
|
1101
|
+
incomplete: boolean
|
|
1102
|
+
}
|
|
1103
|
+
|
|
1104
|
+
/** 点级告警(preview.warnings)→ 展示行;界面在面板级渲染一次(点级告警不归属任何段,绝不猜)。 */
|
|
1105
|
+
export function rollbackPreviewWarnings(preview: RollbackPreviewResult | null): RollbackGuidanceRowView[] {
|
|
1106
|
+
return rollbackGuidanceRows(preview?.warnings)
|
|
1107
|
+
}
|
|
1108
|
+
|
|
1109
|
+
/** 点级指引(preview.guidance)→ 展示行(同上,面板级渲染一次)。 */
|
|
1110
|
+
export function rollbackPreviewGuidance(preview: RollbackPreviewResult | null): RollbackGuidanceRowView[] {
|
|
1111
|
+
return rollbackGuidanceRows(preview?.guidance)
|
|
1112
|
+
}
|
|
1113
|
+
|
|
1114
|
+
/* ------------------------------------------------------------ T15:预览态「无事可做」的四态分派 */
|
|
1115
|
+
|
|
1116
|
+
/**
|
|
1117
|
+
* 预览态那句「工作区怎样怎样」的**四态**判别(T15)。
|
|
1118
|
+
*
|
|
1119
|
+
* 背景(用户截图抱怨的那一屏就是**预览态**):点「回退」之前,三种**完全不同**的事实
|
|
1120
|
+
* 被同一句「工作区与该回退点一致,无需恢复」概括 —— 其中两种是不实陈述:
|
|
1121
|
+
* · 该点压根没记录任何文件(工作区段 files=0);
|
|
1122
|
+
* · 工作区态没有可用记录(available=false);
|
|
1123
|
+
* · 而真正「一致」时,若**会话仍会回退**(fork 子会话),说「无需恢复」也是假尾句。
|
|
1124
|
+
*
|
|
1125
|
+
* 判据**只来自 preview 的字段**(available / overwrite / different / missing / reasonCode /
|
|
1126
|
+
* session.restorable)与宿主给的 canRestore,**禁止解析 detail**(技术细节不是接口)。
|
|
1127
|
+
* 优先级(与验收逐字一致):① 工作区态不可用 → ⓪ 该点**未记录**的新建内容会被删除
|
|
1128
|
+
* (`removalsCount` > 0 = 真会删,宿主 removeCreated 缺省为真)→ ② available 且 overwrite===0
|
|
1129
|
+
* → ③a 工作区一致但会话仍可回退 → ③b 工作区一致且无事可做。另有前置的一条**不属于四态**:
|
|
1130
|
+
* 工作区**真会改动**(different>0 / missing>0 / 会删掉未记录的新建文件)时不上屏任何
|
|
1131
|
+
* 「一致/无需恢复」句(由文件列表说话)——
|
|
1132
|
+
* 这也是「假尾句禁令」在函数层面的落点:`changesSomething === true` 时,
|
|
1133
|
+
* 要么 `key === null`,要么该键的文案**不含**「无需恢复」。
|
|
1134
|
+
*/
|
|
1135
|
+
export type RollbackPreviewNoteKind =
|
|
1136
|
+
/** 工作区真会改动(写文件**或删掉该点未记录的新建文件**)⇒ 没有「一致」句可说,由文件列表说话。 */
|
|
1137
|
+
| 'workspace-changes'
|
|
1138
|
+
/** ① 工作区态没有可用记录(available=false)。 */
|
|
1139
|
+
| 'workspace-unavailable'
|
|
1140
|
+
/** ② 该点没记录任何文件(available=true 且 overwrite===0)。 */
|
|
1141
|
+
| 'no-recorded-files'
|
|
1142
|
+
/** ③a 工作区与该点一致,但会话仍会回退(fork 子会话)。 */
|
|
1143
|
+
| 'workspace-consistent-session'
|
|
1144
|
+
/** ③b 工作区与该点一致,且无事可做(会话也不可回退)。 */
|
|
1145
|
+
| 'workspace-consistent-noop'
|
|
1146
|
+
|
|
1147
|
+
/** 预览态那句文案的判定结果(界面只负责把它翻译上屏,不在界面里重写判据)。 */
|
|
1148
|
+
export interface RollbackPreviewNote {
|
|
1149
|
+
kind: RollbackPreviewNoteKind
|
|
1150
|
+
/** 上屏文案键;**null = 本情形没有该类句子**(工作区有变更 ⇒ 由文件列表说话)。 */
|
|
1151
|
+
key: string | null
|
|
1152
|
+
/** 需要插进 `{reason}` 的**字典键**(只有 ① 用;未知/缺失一律 `rollback.code.unknown`)。 */
|
|
1153
|
+
reasonKey: string | null
|
|
1154
|
+
/** 点「回退」是否**真会改动什么**(工作区会写文件 or 会话会 fork)—— 假尾句禁令的判据。 */
|
|
1155
|
+
changesSomething: boolean
|
|
1156
|
+
/** 会话仍会回退(= 会 fork 出新子会话)。 */
|
|
1157
|
+
sessionForks: boolean
|
|
1158
|
+
}
|
|
1159
|
+
|
|
1160
|
+
/** ① 的原因键来源:workspace.reasonCode 优先 → 段摘要里的 workspace 段 → 未知(绝不编原因)。 */
|
|
1161
|
+
function previewNoteReasonKey(
|
|
1162
|
+
workspace: RollbackWorkspacePreview | null | undefined,
|
|
1163
|
+
segments: readonly { state?: string; reasonCode?: string | null }[] | undefined,
|
|
1164
|
+
): string {
|
|
1165
|
+
const direct = workspace?.reasonCode
|
|
1166
|
+
if (typeof direct === 'string' && direct !== '') return rollbackCodeKey(direct)
|
|
1167
|
+
const segment = segments?.find((item) => item.state === 'workspace')
|
|
1168
|
+
const code = segment?.reasonCode
|
|
1169
|
+
if (typeof code === 'string' && code !== '') return rollbackCodeKey(code)
|
|
1170
|
+
return ROLLBACK_CODE_UNKNOWN_KEY
|
|
1171
|
+
}
|
|
1172
|
+
|
|
1173
|
+
/**
|
|
1174
|
+
* 预览态文案判定(**一处实现**:聊天对话框调用它;将来设置页若要复用同一口径也走这里)。
|
|
1175
|
+
*
|
|
1176
|
+
* @param input.workspace preview.workspace(缺失按「不可用」处理:没有记录信息 ⇒ 不能说「一致」)
|
|
1177
|
+
* @param input.session preview.session(只有 `restorable === true` 才算「会话仍会回退」)
|
|
1178
|
+
* @param input.segments 段摘要(`preview.point.segments`;只用于 ① 的原因键回落,读 reasonCode 字段)
|
|
1179
|
+
* @param input.canRestore 宿主/判定层的回退门(缺省按 true;传 false 时不得声称「会话仍会回退」)
|
|
1180
|
+
*/
|
|
1181
|
+
export function rollbackPreviewNote(input: {
|
|
1182
|
+
workspace?: RollbackWorkspacePreview | null
|
|
1183
|
+
session?: RollbackSessionPreview | null
|
|
1184
|
+
segments?: readonly { state?: string; reasonCode?: string | null }[] | null
|
|
1185
|
+
canRestore?: boolean
|
|
1186
|
+
}): RollbackPreviewNote {
|
|
1187
|
+
const workspace = input.workspace ?? null
|
|
1188
|
+
const session = input.session ?? null
|
|
1189
|
+
const overwrite = countOf(workspace?.overwrite)
|
|
1190
|
+
const different = countOf(workspace?.different)
|
|
1191
|
+
const missing = countOf(workspace?.missing)
|
|
1192
|
+
/**
|
|
1193
|
+
* **会被删除**的条数(= 该点未记录 **且**过了安全判据的那些)。
|
|
1194
|
+
*
|
|
1195
|
+
* 为什么它必须参与「这句话说得说不说得」:宿主 rewind 的 `removeCreated` **缺省为真**
|
|
1196
|
+
* (src/routes/recovery.ts:459),所以「工作区与该点一致,无需恢复」在会删文件的点上同样是**假尾句**
|
|
1197
|
+
* —— 点下去真的会删文件(用户 2026-10-12 第 6 条)。
|
|
1198
|
+
*
|
|
1199
|
+
* 判据**只能**取 `removalsCount` / `removals`(引擎 `planCreatedRemovals` 的同一份规划):
|
|
1200
|
+
* 拿「未记录总数」(`createdAfter` / `leftovers`)当删除数会**双向出错** —— 它把永不删除的
|
|
1201
|
+
* `.git` 路径也算进去,又把证明名单不完整时「一条都不删」的情形说成「工作区会变」。
|
|
1202
|
+
*/
|
|
1203
|
+
const removals = Math.max(
|
|
1204
|
+
countOf(workspace?.removalsCount),
|
|
1205
|
+
Array.isArray(workspace?.removals) ? workspace.removals.length : 0,
|
|
1206
|
+
)
|
|
1207
|
+
// 「会话仍会回退」两个条件都要:该段可回退 + 该点整体可回退(受保护点/两态不全时按钮本来就点不了,
|
|
1208
|
+
// 这时说「会话仍会回退到该点之前」同样是假话)
|
|
1209
|
+
const sessionForks = input.canRestore !== false && session?.restorable === true
|
|
1210
|
+
|
|
1211
|
+
// ① 工作区态没有可用记录
|
|
1212
|
+
if (workspace?.available !== true) {
|
|
1213
|
+
const reasonKey = previewNoteReasonKey(workspace, input.segments ?? undefined)
|
|
1214
|
+
return {
|
|
1215
|
+
kind: 'workspace-unavailable',
|
|
1216
|
+
key: sessionForks ? 'rollback.preview.files.unavailableSession' : 'rollback.preview.files.unavailable',
|
|
1217
|
+
reasonKey,
|
|
1218
|
+
changesSomething: sessionForks,
|
|
1219
|
+
sessionForks,
|
|
1220
|
+
}
|
|
1221
|
+
}
|
|
1222
|
+
// ⓪ 工作区态可用,但该点**未记录**的新建内容会被删掉 ⇒ 工作区真会变,
|
|
1223
|
+
// 「一致 / 无需恢复」在这里也是假尾句(宁可不说话:删掉的文件由列表逐条说出来)。
|
|
1224
|
+
if (removals > 0) {
|
|
1225
|
+
return { kind: 'workspace-changes', key: null, reasonKey: null, changesSomething: true, sessionForks }
|
|
1226
|
+
}
|
|
1227
|
+
// ② 该点没记录任何文件
|
|
1228
|
+
if (overwrite === 0) {
|
|
1229
|
+
return {
|
|
1230
|
+
kind: 'no-recorded-files',
|
|
1231
|
+
key: sessionForks ? 'rollback.preview.files.noFilesSession' : 'rollback.preview.result.noFiles',
|
|
1232
|
+
reasonKey: null,
|
|
1233
|
+
changesSomething: sessionForks,
|
|
1234
|
+
sessionForks,
|
|
1235
|
+
}
|
|
1236
|
+
}
|
|
1237
|
+
// 工作区真会写文件 ⇒ 没有「一致」句(由文件列表说话);这也是假尾句禁令的前置保护
|
|
1238
|
+
if (different > 0 || missing > 0) {
|
|
1239
|
+
return { kind: 'workspace-changes', key: null, reasonKey: null, changesSomething: true, sessionForks }
|
|
1240
|
+
}
|
|
1241
|
+
// ③a 工作区一致,但会话仍会回退 ⇒ 只说工作区(**禁止**「无需恢复」:点下去真会 fork)
|
|
1242
|
+
if (sessionForks) {
|
|
1243
|
+
return { kind: 'workspace-consistent-session', key: 'rollback.preview.files.consistentSession', reasonKey: null, changesSomething: true, sessionForks: true }
|
|
1244
|
+
}
|
|
1245
|
+
// ③b 工作区一致且无事可做
|
|
1246
|
+
return { kind: 'workspace-consistent-noop', key: 'rollback.preview.files.empty', reasonKey: null, changesSomething: false, sessionForks: false }
|
|
1247
|
+
}
|
|
1248
|
+
|
|
1249
|
+
/**
|
|
1250
|
+
* 预览里的「可回退 / 可删除」判定。
|
|
1251
|
+
*
|
|
1252
|
+
* **原因键按宿主真实形状分派**(引擎 preview 恒回 `code:'ok'`,靠 canRestore/canRemove 表达拒绝 ——
|
|
1253
|
+
* 见 src/core/rollback-point.ts:1530-1531:`canRemove = !protected && kind!=='guard'`(删除门)与
|
|
1254
|
+
* `canRestore = !protected && !incomplete && (workspaceRestorable || sessionRestorable)`(回退门)**互相独立**):
|
|
1255
|
+
* ① 受保护点 → rollback.code.protected-point(引擎 remove/rewind 都拒)
|
|
1256
|
+
* ② 两态不全 → rollback.preview.reason.incomplete(引擎要 allowPartial 才放行)
|
|
1257
|
+
* ③ 一段都不可回退 → 段自己的 reasonCode;没有就 rollback.preview.none
|
|
1258
|
+
* ④ 宿主给了非 ok 的 code → 该 code 的键
|
|
1259
|
+
* ⑤ 兜底:救援点 → rollback.preview.reason.guard(宿主拒绝了本可回退的救援点);其余 → rollback.preview.reason.hostRefused
|
|
1260
|
+
* —— **绝不回落 rollback.code.unknown**
|
|
1261
|
+
* 救援点(guard)在 ①②③ 之外**是可回退的**(t22):它只在宿主真的拒绝时落到 ⑤。
|
|
1262
|
+
*/
|
|
1263
|
+
export function rollbackPreviewVerdict(preview: RollbackPreviewResult | null): RollbackPreviewVerdict {
|
|
1264
|
+
if (preview === null) {
|
|
1265
|
+
return { canRestore: false, canRemove: false, reasonKey: ROLLBACK_CODE_UNKNOWN_KEY, mode: 'files+session', segments: [], incomplete: false }
|
|
1266
|
+
}
|
|
1267
|
+
const segments: RollbackState[] = []
|
|
1268
|
+
if (preview.workspace !== undefined && preview.workspace !== null && segmentAvailable(preview.workspace)) segments.push('workspace')
|
|
1269
|
+
if (preview.session !== undefined && preview.session !== null && segmentAvailable(preview.session)) segments.push('session')
|
|
1270
|
+
const point = preview.point ?? preview.summary ?? null
|
|
1271
|
+
const protectedPoint = point?.protected === true
|
|
1272
|
+
const guardPoint = point?.kind === 'guard'
|
|
1273
|
+
const incomplete = point?.incomplete === true
|
|
1274
|
+
const canRestore = preview.canRestore === true && segments.length > 0
|
|
1275
|
+
let reasonKey: string | null = null
|
|
1276
|
+
if (!canRestore) {
|
|
1277
|
+
const segmentReason = preview.session?.reasonCode ?? preview.workspace?.reasonCode
|
|
1278
|
+
if (protectedPoint) reasonKey = 'rollback.code.protected-point'
|
|
1279
|
+
else if (incomplete) reasonKey = ROLLBACK_PREVIEW_REASON_INCOMPLETE_KEY
|
|
1280
|
+
else if (segments.length === 0) {
|
|
1281
|
+
reasonKey = typeof segmentReason === 'string' && segmentReason !== '' ? rollbackCodeKey(segmentReason) : 'rollback.preview.none'
|
|
1282
|
+
} else if (preview.code !== undefined && preview.code !== '' && preview.code !== 'ok') reasonKey = rollbackCodeKey(preview.code)
|
|
1283
|
+
// 兜底:宿主拒绝了一个「看起来可恢复」的点。救援点在这里单独说清(它本来是可回退目标,t22)——
|
|
1284
|
+
// 绝不把它写成「受保护」或「一律不可回退」。
|
|
1285
|
+
else reasonKey = guardPoint ? ROLLBACK_PREVIEW_REASON_GUARD_KEY : ROLLBACK_PREVIEW_REASON_HOST_REFUSED_KEY
|
|
1286
|
+
}
|
|
1287
|
+
return {
|
|
1288
|
+
canRestore,
|
|
1289
|
+
canRemove: preview.canRemove === true,
|
|
1290
|
+
reasonKey,
|
|
1291
|
+
mode: rollbackModeFromSegments(segments),
|
|
1292
|
+
segments,
|
|
1293
|
+
incomplete,
|
|
1294
|
+
}
|
|
1295
|
+
}
|
|
1296
|
+
|
|
1297
|
+
/* ------------------------------------------------------------ 回退结果 → 展示行 */
|
|
1298
|
+
|
|
1299
|
+
export interface RollbackRewindRowView {
|
|
1300
|
+
state: string
|
|
1301
|
+
labelKey: string
|
|
1302
|
+
statusKey: string
|
|
1303
|
+
badgeKind: 'ok' | 'warn' | 'error'
|
|
1304
|
+
restored: number
|
|
1305
|
+
failed: number
|
|
1306
|
+
/** 该点**未记录**、**未删除**的内容数(引擎 scanLeftovers 语义,不是「之后新建」) */
|
|
1307
|
+
leftoversCount: number
|
|
1308
|
+
/** 真的被删掉的「回退点之后新建」条数(段结果里的 removed) */
|
|
1309
|
+
removedCount: number
|
|
1310
|
+
/** 同上,路径清单(已脱敏;宿主上限 20 条) */
|
|
1311
|
+
leftovers: string[]
|
|
1312
|
+
leftoversTruncated: boolean
|
|
1313
|
+
/** 会话段:fork 出来的子会话 id(无则 null) */
|
|
1314
|
+
childSessionId: string | null
|
|
1315
|
+
/** 逐条事实:**渲染前已逐条过 redact** */
|
|
1316
|
+
detail: string[]
|
|
1317
|
+
guidance: RollbackGuidanceRowView[]
|
|
1318
|
+
}
|
|
1319
|
+
|
|
1320
|
+
export function rollbackRewindRows(result: RollbackRewindResult | null): RollbackRewindRowView[] {
|
|
1321
|
+
if (result === null) return []
|
|
1322
|
+
const segments = Array.isArray(result.segments) ? result.segments : []
|
|
1323
|
+
return segments.map((segment) => {
|
|
1324
|
+
const status: RollbackSegmentStatus | 'unknown' =
|
|
1325
|
+
segment.status === 'restored' ? 'restored' : segment.status === 'skipped' ? 'skipped' : segment.status === 'failed' ? 'failed' : 'unknown'
|
|
1326
|
+
const statusKey = status === 'unknown' ? 'rollback.segment.unknown' : 'rollback.segment.' + status
|
|
1327
|
+
const badgeKind: 'ok' | 'warn' | 'error' = status === 'restored' ? 'ok' : status === 'skipped' ? 'warn' : 'error'
|
|
1328
|
+
const leftovers = redactLines(segment.leftovers)
|
|
1329
|
+
const childSessionId = typeof segment.childSessionId === 'string' && segment.childSessionId !== '' ? segment.childSessionId : null
|
|
1330
|
+
return {
|
|
1331
|
+
state: segment.state,
|
|
1332
|
+
labelKey: rollbackStateKey(segment.state),
|
|
1333
|
+
statusKey,
|
|
1334
|
+
badgeKind,
|
|
1335
|
+
restored: numberOrNull(segment.restored) ?? 0,
|
|
1336
|
+
failed: numberOrNull(segment.failed) ?? 0,
|
|
1337
|
+
leftoversCount: Math.max(leftovers.length, countOf(segment.createdAfter)),
|
|
1338
|
+
removedCount: countOf(segment.removed),
|
|
1339
|
+
leftovers,
|
|
1340
|
+
leftoversTruncated: segment.leftoversTruncated === true || segment.createdAfterTruncated === true,
|
|
1341
|
+
childSessionId,
|
|
1342
|
+
detail: redactLines(segment.detail),
|
|
1343
|
+
guidance: rollbackGuidanceRows(segment.guidance),
|
|
1344
|
+
}
|
|
1345
|
+
})
|
|
1346
|
+
}
|
|
1347
|
+
|
|
1348
|
+
/** 回退总体结论 → 字典键 + 徽章语义(未知回落 unknown,绝不裸渲染 outcome)。 */
|
|
1349
|
+
export function rollbackOutcome(outcome: string | null | undefined): { key: string; badgeKind: 'ok' | 'warn' | 'error' } {
|
|
1350
|
+
if (outcome === 'restored') return { key: 'rollback.outcome.restored', badgeKind: 'ok' }
|
|
1351
|
+
if (outcome === 'partial') return { key: 'rollback.outcome.partial', badgeKind: 'warn' }
|
|
1352
|
+
if (outcome === 'failed' || outcome === 'denied') return { key: 'rollback.outcome.' + outcome, badgeKind: 'error' }
|
|
1353
|
+
return { key: 'rollback.outcome.unknown', badgeKind: 'error' }
|
|
1354
|
+
}
|
|
1355
|
+
|
|
1356
|
+
export interface RollbackRewindCounts {
|
|
1357
|
+
restored: number
|
|
1358
|
+
failed: number
|
|
1359
|
+
leftovers: number
|
|
1360
|
+
/** 本次真的删掉的「回退点之后新建」路径数(用户 2026-10-11 定案 A) */
|
|
1361
|
+
removed: number
|
|
1362
|
+
}
|
|
1363
|
+
|
|
1364
|
+
/** 回退结果汇总计数(界面一句话说清「恢复多少 / 失败多少 / 没删多少」)。 */
|
|
1365
|
+
export function rollbackRewindCounts(result: RollbackRewindResult | null): RollbackRewindCounts {
|
|
1366
|
+
const rows = rollbackRewindRows(result)
|
|
1367
|
+
let restored = 0
|
|
1368
|
+
let failed = 0
|
|
1369
|
+
let leftovers = 0
|
|
1370
|
+
let removed = 0
|
|
1371
|
+
for (const row of rows) {
|
|
1372
|
+
restored += row.restored
|
|
1373
|
+
failed += row.failed
|
|
1374
|
+
leftovers += row.leftoversCount
|
|
1375
|
+
removed += row.removedCount
|
|
1376
|
+
}
|
|
1377
|
+
return { restored, failed, leftovers, removed }
|
|
1378
|
+
}
|
|
1379
|
+
|
|
1380
|
+
/**
|
|
1381
|
+
* 会话回退的 fork 结果(S3):原会话日志**只读**,回退产出的是一个新子会话。
|
|
1382
|
+
* 返回 null = 本次没有会话回退(界面不渲染这一行,绝不编造子会话 id)。
|
|
1383
|
+
*/
|
|
1384
|
+
export function rollbackChildSession(result: RollbackRewindResult | null): { key: string; id: string } | null {
|
|
1385
|
+
const direct = result?.childSessionId
|
|
1386
|
+
if (typeof direct === 'string' && direct !== '') return { key: 'rollback.rewind.result.child', id: direct }
|
|
1387
|
+
for (const segment of result?.segments ?? []) {
|
|
1388
|
+
const id = segment.childSessionId
|
|
1389
|
+
if (typeof id === 'string' && id !== '') return { key: 'rollback.rewind.result.child', id }
|
|
1390
|
+
}
|
|
1391
|
+
return null
|
|
1392
|
+
}
|
|
1393
|
+
|
|
1394
|
+
/** 本次因前段失败 / 未请求而未执行的段(只可能是两态;未知段回落 unknown 键)。 */
|
|
1395
|
+
export function rollbackSkippedSegments(result: RollbackRewindResult | null): { state: string; key: string }[] {
|
|
1396
|
+
const segments = Array.isArray(result?.skippedSegments) ? result!.skippedSegments! : []
|
|
1397
|
+
return segments.map((state) => ({ state, key: rollbackStateKey(state) }))
|
|
1398
|
+
}
|
|
1399
|
+
|
|
1400
|
+
/* ------------------------------------------------------------ 文案键常量(面板复用) */
|
|
1401
|
+
|
|
1402
|
+
/** 删除成功文案键(delete 的 `code:'deleted'`)。 */
|
|
1403
|
+
export const ROLLBACK_DELETED_KEY = 'rollback.code.deleted'
|
|
1404
|
+
/** 「没有可回退的内容」文案键(预览判定为否时的兜底原因)。 */
|
|
1405
|
+
export const ROLLBACK_PREVIEW_NONE_KEY = 'rollback.preview.none'
|