@logictan/dsh-config-manager 0.1.60

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (429) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +105 -0
  3. package/cordis.patch.yml +21 -0
  4. package/lib/adapters/agent-instructions.d.ts +13 -0
  5. package/lib/adapters/agent-instructions.js +46 -0
  6. package/lib/adapters/agent-presets.d.ts +13 -0
  7. package/lib/adapters/agent-presets.js +14 -0
  8. package/lib/adapters/credentials.d.ts +37 -0
  9. package/lib/adapters/credentials.js +122 -0
  10. package/lib/adapters/file-collection.d.ts +15 -0
  11. package/lib/adapters/file-collection.js +127 -0
  12. package/lib/adapters/index.d.ts +57 -0
  13. package/lib/adapters/index.js +55 -0
  14. package/lib/adapters/link-report.d.ts +16 -0
  15. package/lib/adapters/link-report.js +47 -0
  16. package/lib/adapters/mcp.d.ts +28 -0
  17. package/lib/adapters/mcp.js +178 -0
  18. package/lib/adapters/plugin-files.d.ts +18 -0
  19. package/lib/adapters/plugin-files.js +142 -0
  20. package/lib/adapters/plugins.d.ts +41 -0
  21. package/lib/adapters/plugins.js +603 -0
  22. package/lib/adapters/pnpm-workspace.d.ts +26 -0
  23. package/lib/adapters/pnpm-workspace.js +147 -0
  24. package/lib/adapters/prompts.d.ts +30 -0
  25. package/lib/adapters/prompts.js +231 -0
  26. package/lib/adapters/providers.d.ts +34 -0
  27. package/lib/adapters/providers.js +155 -0
  28. package/lib/adapters/self.d.ts +17 -0
  29. package/lib/adapters/self.js +71 -0
  30. package/lib/adapters/sessions.d.ts +14 -0
  31. package/lib/adapters/sessions.js +15 -0
  32. package/lib/adapters/settings.d.ts +28 -0
  33. package/lib/adapters/settings.js +165 -0
  34. package/lib/adapters/skills.d.ts +13 -0
  35. package/lib/adapters/skills.js +14 -0
  36. package/lib/adapters/test-helpers.d.ts +125 -0
  37. package/lib/adapters/test-helpers.js +236 -0
  38. package/lib/adapters/ui.d.ts +27 -0
  39. package/lib/adapters/ui.js +89 -0
  40. package/lib/adapters/workspaces.d.ts +13 -0
  41. package/lib/adapters/workspaces.js +106 -0
  42. package/lib/client.d.ts +1026 -0
  43. package/lib/client.js +10047 -0
  44. package/lib/core/analyzer.d.ts +99 -0
  45. package/lib/core/analyzer.js +934 -0
  46. package/lib/core/backup.d.ts +138 -0
  47. package/lib/core/backup.js +513 -0
  48. package/lib/core/boot-rescue.d.ts +146 -0
  49. package/lib/core/boot-rescue.js +618 -0
  50. package/lib/core/cache-cleaner.d.ts +31 -0
  51. package/lib/core/cache-cleaner.js +95 -0
  52. package/lib/core/config-lifecycle.d.ts +180 -0
  53. package/lib/core/config-lifecycle.js +481 -0
  54. package/lib/core/config-snapshot.d.ts +135 -0
  55. package/lib/core/config-snapshot.js +424 -0
  56. package/lib/core/config-state.d.ts +70 -0
  57. package/lib/core/config-state.js +177 -0
  58. package/lib/core/consult-source.d.ts +58 -0
  59. package/lib/core/consult-source.js +260 -0
  60. package/lib/core/crash-report.d.ts +84 -0
  61. package/lib/core/crash-report.js +317 -0
  62. package/lib/core/exporter.d.ts +68 -0
  63. package/lib/core/exporter.js +375 -0
  64. package/lib/core/ghost-sweep.d.ts +19 -0
  65. package/lib/core/ghost-sweep.js +52 -0
  66. package/lib/core/importer.d.ts +60 -0
  67. package/lib/core/importer.js +29 -0
  68. package/lib/core/index.d.ts +14 -0
  69. package/lib/core/index.js +14 -0
  70. package/lib/core/journal.d.ts +197 -0
  71. package/lib/core/journal.js +418 -0
  72. package/lib/core/local-plugin-host.d.ts +35 -0
  73. package/lib/core/local-plugin-host.js +120 -0
  74. package/lib/core/local-plugin-pack.d.ts +135 -0
  75. package/lib/core/local-plugin-pack.js +278 -0
  76. package/lib/core/messages.d.ts +310 -0
  77. package/lib/core/messages.js +621 -0
  78. package/lib/core/migration-consult.d.ts +117 -0
  79. package/lib/core/migration-consult.js +363 -0
  80. package/lib/core/migration-history.d.ts +153 -0
  81. package/lib/core/migration-history.js +411 -0
  82. package/lib/core/model-tools.d.ts +50 -0
  83. package/lib/core/model-tools.js +148 -0
  84. package/lib/core/msg-types.d.ts +8 -0
  85. package/lib/core/msg-types.js +6 -0
  86. package/lib/core/phase3-child-crash.d.ts +1 -0
  87. package/lib/core/phase3-child-crash.js +87 -0
  88. package/lib/core/phase3-host.d.ts +138 -0
  89. package/lib/core/phase3-host.js +359 -0
  90. package/lib/core/phase3-prod-child.d.ts +1 -0
  91. package/lib/core/phase3-prod-child.js +35 -0
  92. package/lib/core/phase4-crash-child.d.ts +1 -0
  93. package/lib/core/phase4-crash-child.js +103 -0
  94. package/lib/core/plugin-cli.d.ts +150 -0
  95. package/lib/core/plugin-cli.js +497 -0
  96. package/lib/core/reconcile.d.ts +120 -0
  97. package/lib/core/reconcile.js +389 -0
  98. package/lib/core/recovery-orchestrator.d.ts +94 -0
  99. package/lib/core/recovery-orchestrator.js +327 -0
  100. package/lib/core/restore.d.ts +162 -0
  101. package/lib/core/restore.js +703 -0
  102. package/lib/core/rollback.d.ts +12 -0
  103. package/lib/core/rollback.js +145 -0
  104. package/lib/core/run-registry.d.ts +85 -0
  105. package/lib/core/run-registry.js +157 -0
  106. package/lib/core/startup-barrier.d.ts +53 -0
  107. package/lib/core/startup-barrier.js +77 -0
  108. package/lib/core/types.d.ts +515 -0
  109. package/lib/core/types.js +27 -0
  110. package/lib/core/undo.d.ts +73 -0
  111. package/lib/core/undo.js +87 -0
  112. package/lib/core/validator.d.ts +18 -0
  113. package/lib/core/validator.js +77 -0
  114. package/lib/core/verify-recovery.d.ts +21 -0
  115. package/lib/core/verify-recovery.js +256 -0
  116. package/lib/core/watcher.d.ts +101 -0
  117. package/lib/core/watcher.js +219 -0
  118. package/lib/index.d.ts +238 -0
  119. package/lib/index.js +2764 -0
  120. package/lib/migrations/index.d.ts +29 -0
  121. package/lib/migrations/index.js +56 -0
  122. package/lib/migrations/v1-to-v2.d.ts +9 -0
  123. package/lib/migrations/v1-to-v2.js +10 -0
  124. package/lib/profiles/index.d.ts +4 -0
  125. package/lib/profiles/index.js +5 -0
  126. package/lib/profiles/profile-manager.d.ts +106 -0
  127. package/lib/profiles/profile-manager.js +543 -0
  128. package/lib/schema/config.d.ts +28 -0
  129. package/lib/schema/config.js +165 -0
  130. package/lib/schema/index.d.ts +24 -0
  131. package/lib/schema/index.js +26 -0
  132. package/lib/schema/manifest.d.ts +28 -0
  133. package/lib/schema/manifest.js +109 -0
  134. package/lib/schema/tombstones.d.ts +46 -0
  135. package/lib/schema/tombstones.js +64 -0
  136. package/lib/schema/types.d.ts +232 -0
  137. package/lib/schema/types.js +8 -0
  138. package/lib/schema/versions.d.ts +27 -0
  139. package/lib/schema/versions.js +50 -0
  140. package/lib/security/encryption.d.ts +85 -0
  141. package/lib/security/encryption.js +279 -0
  142. package/lib/security/index.d.ts +16 -0
  143. package/lib/security/index.js +17 -0
  144. package/lib/security/integrity.d.ts +30 -0
  145. package/lib/security/integrity.js +78 -0
  146. package/lib/security/redaction.d.ts +26 -0
  147. package/lib/security/redaction.js +95 -0
  148. package/lib/security/secret-scanner.d.ts +86 -0
  149. package/lib/security/secret-scanner.js +404 -0
  150. package/lib/security/vault.d.ts +59 -0
  151. package/lib/security/vault.js +151 -0
  152. package/lib/security/zip-security.d.ts +27 -0
  153. package/lib/security/zip-security.js +154 -0
  154. package/lib/sync/autosync-config.d.ts +39 -0
  155. package/lib/sync/autosync-config.js +151 -0
  156. package/lib/sync/autosync-scheduler.d.ts +139 -0
  157. package/lib/sync/autosync-scheduler.js +564 -0
  158. package/lib/sync/fs.d.ts +18 -0
  159. package/lib/sync/fs.js +49 -0
  160. package/lib/sync/git/git-transport.d.ts +100 -0
  161. package/lib/sync/git/git-transport.js +437 -0
  162. package/lib/sync/github-auth.d.ts +131 -0
  163. package/lib/sync/github-auth.js +264 -0
  164. package/lib/sync/layout.d.ts +43 -0
  165. package/lib/sync/layout.js +203 -0
  166. package/lib/sync/snapshot-json.d.ts +9 -0
  167. package/lib/sync/snapshot-json.js +107 -0
  168. package/lib/sync/sync-config.d.ts +81 -0
  169. package/lib/sync/sync-config.js +281 -0
  170. package/lib/sync/sync-engine.d.ts +269 -0
  171. package/lib/sync/sync-engine.js +591 -0
  172. package/lib/sync/sync-history.d.ts +42 -0
  173. package/lib/sync/sync-history.js +78 -0
  174. package/lib/sync/sync-selection.d.ts +29 -0
  175. package/lib/sync/sync-selection.js +127 -0
  176. package/lib/sync/sync-session.d.ts +40 -0
  177. package/lib/sync/sync-session.js +54 -0
  178. package/lib/sync/sync-state.d.ts +37 -0
  179. package/lib/sync/sync-state.js +108 -0
  180. package/lib/sync/transport.d.ts +75 -0
  181. package/lib/sync/transport.js +55 -0
  182. package/lib/sync/ui-prefs.d.ts +34 -0
  183. package/lib/sync/ui-prefs.js +109 -0
  184. package/lib/sync/webdav/webdav-transport.d.ts +77 -0
  185. package/lib/sync/webdav/webdav-transport.js +451 -0
  186. package/lib/ui/errors.d.ts +19 -0
  187. package/lib/ui/errors.js +81 -0
  188. package/lib/ui/export-flow.d.ts +82 -0
  189. package/lib/ui/export-flow.js +98 -0
  190. package/lib/ui/i18n.d.ts +289 -0
  191. package/lib/ui/i18n.js +581 -0
  192. package/lib/ui/migration-consult-view.d.ts +91 -0
  193. package/lib/ui/migration-consult-view.js +115 -0
  194. package/lib/ui/progress.d.ts +38 -0
  195. package/lib/ui/progress.js +87 -0
  196. package/lib/ui/report.d.ts +23 -0
  197. package/lib/ui/report.js +153 -0
  198. package/lib/ui/test-helpers.d.ts +69 -0
  199. package/lib/ui/test-helpers.js +154 -0
  200. package/lib/ui/types.d.ts +257 -0
  201. package/lib/ui/types.js +21 -0
  202. package/lib/utils/atomic-write.d.ts +88 -0
  203. package/lib/utils/atomic-write.js +399 -0
  204. package/lib/utils/env-lock.d.ts +393 -0
  205. package/lib/utils/env-lock.js +930 -0
  206. package/lib/utils/hashing.d.ts +17 -0
  207. package/lib/utils/hashing.js +41 -0
  208. package/lib/utils/json.d.ts +24 -0
  209. package/lib/utils/json.js +76 -0
  210. package/lib/utils/logger.d.ts +39 -0
  211. package/lib/utils/logger.js +82 -0
  212. package/lib/utils/paths.d.ts +42 -0
  213. package/lib/utils/paths.js +189 -0
  214. package/lib/utils/proxy.d.ts +69 -0
  215. package/lib/utils/proxy.js +455 -0
  216. package/lib/utils/recursive-walk.d.ts +24 -0
  217. package/lib/utils/recursive-walk.js +134 -0
  218. package/lib/utils/zip.d.ts +54 -0
  219. package/lib/utils/zip.js +285 -0
  220. package/package.json +188 -0
  221. package/src/adapters/agent-instructions.ts +48 -0
  222. package/src/adapters/agent-presets.ts +14 -0
  223. package/src/adapters/credentials.test.ts +90 -0
  224. package/src/adapters/credentials.ts +143 -0
  225. package/src/adapters/file-collection.ts +137 -0
  226. package/src/adapters/files.test.ts +353 -0
  227. package/src/adapters/index.ts +94 -0
  228. package/src/adapters/link-report.test.ts +65 -0
  229. package/src/adapters/link-report.ts +61 -0
  230. package/src/adapters/mcp.test.ts +96 -0
  231. package/src/adapters/mcp.ts +185 -0
  232. package/src/adapters/plugin-files.ts +146 -0
  233. package/src/adapters/plugins.test.ts +326 -0
  234. package/src/adapters/plugins.ts +615 -0
  235. package/src/adapters/pnpm-workspace.test.ts +87 -0
  236. package/src/adapters/pnpm-workspace.ts +150 -0
  237. package/src/adapters/prompts.test.ts +110 -0
  238. package/src/adapters/prompts.ts +230 -0
  239. package/src/adapters/providers.test.ts +126 -0
  240. package/src/adapters/providers.ts +173 -0
  241. package/src/adapters/roundtrip.test.ts +190 -0
  242. package/src/adapters/self.test.ts +92 -0
  243. package/src/adapters/self.ts +75 -0
  244. package/src/adapters/sessions.ts +15 -0
  245. package/src/adapters/settings.test.ts +100 -0
  246. package/src/adapters/settings.ts +187 -0
  247. package/src/adapters/skills.ts +14 -0
  248. package/src/adapters/test-helpers.ts +248 -0
  249. package/src/adapters/ui.test.ts +57 -0
  250. package/src/adapters/ui.ts +108 -0
  251. package/src/adapters/workspaces.test.ts +106 -0
  252. package/src/adapters/workspaces.ts +109 -0
  253. package/src/client/ConfigManagerSection.tsx +90 -0
  254. package/src/client/api.ts +66 -0
  255. package/src/client/client-types.ts +18 -0
  256. package/src/client/common/ErrorBanner.tsx +73 -0
  257. package/src/client/common/Icon.tsx +129 -0
  258. package/src/client/common/Modal.tsx +173 -0
  259. package/src/client/common/ToastViewport.tsx +73 -0
  260. package/src/client/common/toast-store.test.ts +192 -0
  261. package/src/client/common/toast-store.ts +181 -0
  262. package/src/client/common/toast-types.ts +7 -0
  263. package/src/client/common/ui.tsx +342 -0
  264. package/src/client/config-manager.module.css +2971 -0
  265. package/src/client/css-modules.d.ts +8 -0
  266. package/src/client/index.ts +65 -0
  267. package/src/client/locales.ts +56 -0
  268. package/src/client/lucide-icons.d.ts +38 -0
  269. package/src/client/run-store.test.ts +223 -0
  270. package/src/client/run-store.ts +372 -0
  271. package/src/client/sync/SyncConfirmView.tsx +379 -0
  272. package/src/client/sync/SyncHistoryView.test.ts +40 -0
  273. package/src/client/sync/SyncHistoryView.tsx +220 -0
  274. package/src/client/sync/SyncSettingsView.tsx +1535 -0
  275. package/src/client/sync/history-model.test.ts +194 -0
  276. package/src/client/sync/history-model.ts +195 -0
  277. package/src/client/sync/sync-api.test.ts +585 -0
  278. package/src/client/sync/sync-api.ts +543 -0
  279. package/src/client/sync/sync-locales.ts +416 -0
  280. package/src/client/sync/sync-push-preview.test.ts +52 -0
  281. package/src/client/sync/sync-view-v2.test.ts +213 -0
  282. package/src/client/sync/sync-view.test.ts +487 -0
  283. package/src/client/sync/sync-view.ts +759 -0
  284. package/src/core/analyzer.ts +1068 -0
  285. package/src/core/backup.ts +605 -0
  286. package/src/core/boot-rescue.test.ts +847 -0
  287. package/src/core/boot-rescue.ts +737 -0
  288. package/src/core/cache-cleaner.test.ts +169 -0
  289. package/src/core/cache-cleaner.ts +121 -0
  290. package/src/core/config-lifecycle.test.ts +727 -0
  291. package/src/core/config-lifecycle.ts +573 -0
  292. package/src/core/config-snapshot.test.ts +369 -0
  293. package/src/core/config-snapshot.ts +522 -0
  294. package/src/core/config-state.test.ts +381 -0
  295. package/src/core/config-state.ts +207 -0
  296. package/src/core/consult-source.test.ts +199 -0
  297. package/src/core/consult-source.ts +293 -0
  298. package/src/core/crash-report.test.ts +380 -0
  299. package/src/core/crash-report.ts +369 -0
  300. package/src/core/exporter.ts +417 -0
  301. package/src/core/ghost-sweep.test.ts +94 -0
  302. package/src/core/ghost-sweep.ts +67 -0
  303. package/src/core/importer.ts +83 -0
  304. package/src/core/index.ts +56 -0
  305. package/src/core/journal.test.ts +257 -0
  306. package/src/core/journal.ts +518 -0
  307. package/src/core/local-plugin-host.ts +152 -0
  308. package/src/core/local-plugin-pack.test.ts +359 -0
  309. package/src/core/local-plugin-pack.ts +363 -0
  310. package/src/core/messages.test.ts +54 -0
  311. package/src/core/messages.ts +664 -0
  312. package/src/core/migration-consult.test.ts +315 -0
  313. package/src/core/migration-consult.ts +494 -0
  314. package/src/core/migration-history.test.ts +319 -0
  315. package/src/core/migration-history.ts +484 -0
  316. package/src/core/model-tools.test.ts +190 -0
  317. package/src/core/model-tools.ts +208 -0
  318. package/src/core/msg-types.ts +10 -0
  319. package/src/core/phase3-child-crash.ts +95 -0
  320. package/src/core/phase3-host.ts +411 -0
  321. package/src/core/phase3-p1.test.ts +212 -0
  322. package/src/core/phase3-p2b-fingerprint.test.ts +189 -0
  323. package/src/core/phase3-prod-child.ts +36 -0
  324. package/src/core/phase3-production-integration.test.ts +286 -0
  325. package/src/core/phase4-crash-child.ts +101 -0
  326. package/src/core/phase4-crash-injection.test.ts +166 -0
  327. package/src/core/plugin-cli.fs.test.ts +184 -0
  328. package/src/core/plugin-cli.test.ts +137 -0
  329. package/src/core/plugin-cli.ts +563 -0
  330. package/src/core/reconcile.test.ts +392 -0
  331. package/src/core/reconcile.ts +498 -0
  332. package/src/core/recovery-orchestrator.test.ts +145 -0
  333. package/src/core/recovery-orchestrator.ts +366 -0
  334. package/src/core/restore-manage.test.ts +94 -0
  335. package/src/core/restore.ts +849 -0
  336. package/src/core/rollback.ts +151 -0
  337. package/src/core/run-progress.test.ts +237 -0
  338. package/src/core/run-registry.test.ts +232 -0
  339. package/src/core/run-registry.ts +209 -0
  340. package/src/core/smoke.test.ts +842 -0
  341. package/src/core/startup-barrier.ts +99 -0
  342. package/src/core/types.ts +542 -0
  343. package/src/core/undo.ts +116 -0
  344. package/src/core/validator.ts +84 -0
  345. package/src/core/verify-recovery.ts +265 -0
  346. package/src/core/watcher.test.ts +326 -0
  347. package/src/core/watcher.ts +263 -0
  348. package/src/index.facade.test.ts +265 -0
  349. package/src/index.sync.test.ts +217 -0
  350. package/src/index.ts +2975 -0
  351. package/src/migrations/index.ts +71 -0
  352. package/src/migrations/v1-to-v2.ts +17 -0
  353. package/src/profiles/index.ts +8 -0
  354. package/src/profiles/profile-manager.test.ts +282 -0
  355. package/src/profiles/profile-manager.ts +630 -0
  356. package/src/schema/config.ts +165 -0
  357. package/src/schema/index.ts +71 -0
  358. package/src/schema/manifest-schema.test.ts +519 -0
  359. package/src/schema/manifest.ts +124 -0
  360. package/src/schema/tombstones.test.ts +131 -0
  361. package/src/schema/tombstones.ts +89 -0
  362. package/src/schema/types.ts +245 -0
  363. package/src/schema/versions.ts +55 -0
  364. package/src/security/encryption.ts +335 -0
  365. package/src/security/index.ts +16 -0
  366. package/src/security/integrity.ts +102 -0
  367. package/src/security/redaction.ts +101 -0
  368. package/src/security/secret-scanner.ts +445 -0
  369. package/src/security/security.test.ts +1215 -0
  370. package/src/security/vault.test.ts +164 -0
  371. package/src/security/vault.ts +198 -0
  372. package/src/security/zip-security.ts +180 -0
  373. package/src/sync/autosync-config.test.ts +147 -0
  374. package/src/sync/autosync-config.ts +179 -0
  375. package/src/sync/autosync-scheduler.test.ts +490 -0
  376. package/src/sync/autosync-scheduler.ts +646 -0
  377. package/src/sync/fs.ts +48 -0
  378. package/src/sync/git/git-transport.test.ts +673 -0
  379. package/src/sync/git/git-transport.ts +491 -0
  380. package/src/sync/github-auth.test.ts +280 -0
  381. package/src/sync/github-auth.ts +356 -0
  382. package/src/sync/layout.test.ts +270 -0
  383. package/src/sync/layout.ts +222 -0
  384. package/src/sync/snapshot-json.test.ts +119 -0
  385. package/src/sync/snapshot-json.ts +128 -0
  386. package/src/sync/sync-config.test.ts +310 -0
  387. package/src/sync/sync-config.ts +299 -0
  388. package/src/sync/sync-engine.test.ts +1074 -0
  389. package/src/sync/sync-engine.ts +754 -0
  390. package/src/sync/sync-history.test.ts +116 -0
  391. package/src/sync/sync-history.ts +120 -0
  392. package/src/sync/sync-selection.test.ts +186 -0
  393. package/src/sync/sync-selection.ts +143 -0
  394. package/src/sync/sync-session.test.ts +143 -0
  395. package/src/sync/sync-session.ts +77 -0
  396. package/src/sync/sync-state.test.ts +164 -0
  397. package/src/sync/sync-state.ts +131 -0
  398. package/src/sync/transport.test.ts +111 -0
  399. package/src/sync/transport.ts +118 -0
  400. package/src/sync/ui-prefs.test.ts +152 -0
  401. package/src/sync/ui-prefs.ts +135 -0
  402. package/src/sync/webdav/webdav-transport.test.ts +871 -0
  403. package/src/sync/webdav/webdav-transport.ts +535 -0
  404. package/src/ui/errors.test.ts +47 -0
  405. package/src/ui/errors.ts +100 -0
  406. package/src/ui/export-flow.test.ts +87 -0
  407. package/src/ui/export-flow.ts +156 -0
  408. package/src/ui/i18n.ts +590 -0
  409. package/src/ui/migration-consult-view.test.ts +151 -0
  410. package/src/ui/migration-consult-view.ts +178 -0
  411. package/src/ui/progress.test.ts +47 -0
  412. package/src/ui/progress.ts +102 -0
  413. package/src/ui/report.test.ts +101 -0
  414. package/src/ui/report.ts +146 -0
  415. package/src/ui/test-helpers.ts +184 -0
  416. package/src/ui/types.ts +325 -0
  417. package/src/utils/atomic-write.test.ts +312 -0
  418. package/src/utils/atomic-write.ts +422 -0
  419. package/src/utils/env-lock.test.ts +999 -0
  420. package/src/utils/env-lock.ts +1086 -0
  421. package/src/utils/hashing.ts +53 -0
  422. package/src/utils/json.ts +77 -0
  423. package/src/utils/logger.ts +114 -0
  424. package/src/utils/paths.ts +176 -0
  425. package/src/utils/proxy.test.ts +465 -0
  426. package/src/utils/proxy.ts +493 -0
  427. package/src/utils/recursive-walk.test.ts +148 -0
  428. package/src/utils/recursive-walk.ts +145 -0
  429. package/src/utils/zip.ts +332 -0
@@ -0,0 +1,498 @@
1
+ /**
2
+ * Reconciler(Phase 3:startup reconciliation)。
3
+ *
4
+ * 职责:扫描 active journals → parse/schema 校验(corrupt 隔离)→ 验 environment fingerprint
5
+ * → 验 Journal→Lock 绑定 → 验快照 → 验 step 指纹 → 外部探测 → 选择推荐 recovery outcome
6
+ * → 执行(需用户确认的破坏性动作才执行)→ recovery-history → 幂等。
7
+ *
8
+ * **Reconciler 不拥有 Environment Lock primitive 实现** —— 通过 Coordinator/port 使用
9
+ * (release、active≤1、recover 显式流程由宿主/Coordinator 编排)。
10
+ *
11
+ * **Startup(Rev 3 P1-NEW-2)不自动 recover stale lock**:
12
+ * - 若发现 stale lock + incomplete journal → RECOVERY_REQUIRED / SAFE MODE,不自动 recoverStaleLock;
13
+ * - 用户显式确认后才 prove stale → recover → acquire → reconcile。
14
+ */
15
+ import {
16
+ JournalStore, isTerminalState, transitionJournalState, JOURNAL_SCHEMA_VERSION,
17
+ type OperationJournal, type JournalStep,
18
+ } from './journal.ts';
19
+
20
+ // ---------- 注入 hooks(引擎/宿主具体化) ----------
21
+
22
+ export type StepFingerprintVerdict =
23
+ | 'before-match' // 磁盘 == side effect 前 → 未应用
24
+ | 'after-match' // 磁盘 == afterFp → 已应用
25
+ | 'none' // 与 before/after 都不符 → 混合/未知
26
+ | 'unable'; // 不可指纹(settings/patchLine/workspace)
27
+
28
+ export type ExternalProbe =
29
+ | 'installed' | 'half-installed' | 'reverse-residue' | 'not-installed' | 'unknown';
30
+
31
+ export interface ReconcileProbeHooks {
32
+ /** 对某 step 的目标做磁盘指纹判定。 */
33
+ verifyStepFingerprint(step: JournalStep): Promise<StepFingerprintVerdict>;
34
+ /** 外部步骤(插件/Git/WebDAV)状态探测。 */
35
+ probeExternal(step: JournalStep): Promise<ExternalProbe>;
36
+ /**
37
+ * 快照完整性(id 合法 + snapshot.json 存在 + 必要 blobs 存在 + READY + 属于本 op)。
38
+ * binding 为 journal 的绑定字段(operationId/ownerInstanceId/environmentFingerprint),
39
+ * 实现应校验 snapshot 的 manifest binding 与 journal 一致(P1-1:防伪造快照过 snapshotExists)。
40
+ */
41
+ snapshotExists(
42
+ snapshotId: string | null,
43
+ binding?: { operationId?: string; ownerInstanceId?: string; environmentFingerprint?: string },
44
+ ): Promise<boolean>;
45
+ }
46
+
47
+ export interface ReconcileEnv {
48
+ environmentFingerprint: string;
49
+ /** 判定某 journal 是否属于仍存活的 live owner(live op 不可 reconcile/quarantine/move)。 */
50
+ isLiveOwner(journal: OperationJournal): boolean | Promise<boolean>;
51
+ /**
52
+ * P1-A:期望的 crashed-ownership instanceId(= stale environment.lock owner.instanceId)。
53
+ * 显式 recovery 捕获 stale ownership 证据后传入:active journal 的 ownerInstanceId/lockId
54
+ * 必须匹配该 crashed ownership,才可作为 trusted transaction 参与 reconcile/rollback/resume;
55
+ * 否则 → needs-attention(SAFE MODE,不 trusted),防跨 transaction/伪造 journal。
56
+ */
57
+ expectedOwnershipInstanceId?: string;
58
+ }
59
+
60
+ export interface ReconcileOptions {
61
+ /** 是否自动隔离 corrupt journal(move 到 quarantine + attention sidecar)。默认 true。 */
62
+ autoQuarantine?: boolean;
63
+ /** 是否对 terminal-in-active journal 自动 move 到 completed。默认 true。 */
64
+ organizeTerminal?: boolean;
65
+ }
66
+
67
+ export type ReconcileDecisionKind =
68
+ | 'live' // 活 owner 持有,跳过
69
+ | 'corrupt' // 无法 parse / 非法 schema → quarantine(已隔离)
70
+ | 'env-mismatch' // environmentFingerprint 不符 → 不恢复
71
+ | 'organized' // terminal 遗留 active → 已规整到 completed(无实际 recovery)
72
+ | 'recovered' // 所有 step 已 prove done → 视同 commit(resume 不需重做)
73
+ | 'noop' // 无任何 step 应用 + 无快照可回滚 → 安全丢弃
74
+ | 'rollback-recommended'// 可安全回滚(快照有效)→ 需用户确认后才执行
75
+ | 'rollback-continue' // 回滚中断(ROLLING_BACK/部分 entryDone)→ 需用户确认续跑,绝不判 recovered
76
+ | 'needs-attention' // 不可证明(external / 不可指纹 / 快照缺失 / 冲突)→ SAFE MODE,等用户确认
77
+
78
+ export interface ReconcileDecision {
79
+ operationId: string;
80
+ kind: ReconcileDecisionKind;
81
+ reason: string;
82
+ snapshotId: string | null;
83
+ }
84
+
85
+ /** 汇总结果:是否该进入 SAFE MODE(任一 needs-attention / env-mismatch / corrupt / rollback-recommended)。 */
86
+ export interface ReconcileOutcome {
87
+ decisions: ReconcileDecision[];
88
+ safeModeRequired: boolean;
89
+ /** unresolved(未到 terminal)的 incomplete op id(RECOVERY_REQUIRED 候选)。 */
90
+ unresolved: string[];
91
+ }
92
+
93
+ // ---------- Reconcile ----------
94
+
95
+ /**
96
+ * 分析 active/ 下的所有 journal,返回每个 op 的决策 + 是否需 SAFE MODE。
97
+ * 只读(除 corrupt quarantine 与 terminal organize / recovery-history 外,不做 data mutation)。
98
+ */
99
+ export async function reconcileActive(
100
+ store: JournalStore,
101
+ hooks: ReconcileProbeHooks,
102
+ env: ReconcileEnv,
103
+ opts: ReconcileOptions = {},
104
+ ): Promise<ReconcileOutcome> {
105
+ const decisions: ReconcileDecision[] = [];
106
+ const unresolved: string[] = [];
107
+ let safeModeRequired = false;
108
+
109
+ const activeIds = await store.scanActive();
110
+ for (const operationId of activeIds) {
111
+ const outcome = await reconcileOne(store, hooks, env, opts, operationId);
112
+ decisions.push(outcome.decision);
113
+ if (outcome.safeMode) safeModeRequired = true;
114
+ if (outcome.unresolved) unresolved.push(operationId);
115
+ }
116
+ return { decisions, safeModeRequired, unresolved };
117
+ }
118
+
119
+ async function reconcileOne(
120
+ store: JournalStore,
121
+ hooks: ReconcileProbeHooks,
122
+ env: ReconcileEnv,
123
+ opts: ReconcileOptions,
124
+ operationId: string,
125
+ ): Promise<{ decision: ReconcileDecision; safeMode: boolean; unresolved: boolean }> {
126
+ const j = await store.loadActive(operationId);
127
+ if (j === null) {
128
+ // 存在但 parse/校验失败 → corrupt → quarantine
129
+ const reason = `journal ${operationId} 无法解析或 schema 非法(隔离处理)`;
130
+ if (opts.autoQuarantine !== false) {
131
+ try {
132
+ await store.quarantine(operationId, reason);
133
+ } catch { /* 隔离失败由上层诊断 */ }
134
+ }
135
+ await store.writeSafeMode(true).catch(() => undefined);
136
+ return { decision: { operationId, kind: 'corrupt', reason, snapshotId: null }, safeMode: true, unresolved: true };
137
+ }
138
+
139
+ // live owner 检查
140
+ if (await env.isLiveOwner(j)) {
141
+ return { decision: { operationId, kind: 'live', reason: 'owner 存活,live operation 进行中', snapshotId: j.snapshotId }, safeMode: false, unresolved: false };
142
+ }
143
+
144
+ // P1-A:ownership binding 强制校验(显式 recovery 捕获的 crashed ownership 证据)。
145
+ // journal.ownerInstanceId/lockId 必须匹配期望的 crashed ownership instanceId,否则不作 trusted 恢复。
146
+ if (env.expectedOwnershipInstanceId !== undefined && env.expectedOwnershipInstanceId !== '') {
147
+ const bindingOk = j.ownerInstanceId === env.expectedOwnershipInstanceId
148
+ && j.lockId === env.expectedOwnershipInstanceId; // lockId = 同 epoch identity(ownerInstanceId)
149
+ if (!bindingOk) {
150
+ const reason = `ownership binding 不匹配(journal.ownerInstanceId/lockId 非 crashed ownership ${env.expectedOwnershipInstanceId}),不作 trusted 恢复`;
151
+ await store.writeSafeMode(true).catch(() => undefined);
152
+ await store.appendRecoveryHistory('binding-mismatch', { operationId, at: new Date().toISOString(), reason }).catch(() => undefined);
153
+ return { decision: { operationId, kind: 'needs-attention', reason, snapshotId: j.snapshotId }, safeMode: true, unresolved: true };
154
+ }
155
+ }
156
+
157
+ // env fingerprint
158
+ if (env.environmentFingerprint && j.environmentFingerprint && env.environmentFingerprint !== j.environmentFingerprint) {
159
+ const reason = `environment fingerprint 不匹配(跨机/环境变化),不自动恢复`;
160
+ await store.writeSafeMode(true).catch(() => undefined);
161
+ return { decision: { operationId, kind: 'env-mismatch', reason, snapshotId: j.snapshotId }, safeMode: true, unresolved: true };
162
+ }
163
+
164
+ // terminal 遗留 active → 规整到 completed(无实质 recovery)
165
+ if (isTerminalState(j.state)) {
166
+ if (j.state === 'NEEDS_ATTENTION') {
167
+ // NEEDS_ATTENTION 是「待用户确认」状态,保持 active 可见(不静默规整),SAFE MODE
168
+ await store.writeSafeMode(true).catch(() => undefined);
169
+ return { decision: { operationId, kind: 'needs-attention', reason: 'NEEDS_ATTENTION 待用户确认', snapshotId: j.snapshotId }, safeMode: true, unresolved: true };
170
+ }
171
+ // COMMITTED / ROLLED_BACK / RECOVERED 遗留 → 规整
172
+ if (opts.organizeTerminal !== false) {
173
+ try { await store.moveToCompleted(operationId); } catch { /* move 失败不影响 */ }
174
+ }
175
+ return { decision: { operationId, kind: 'organized', reason: `terminal(${j.state}) journal 规整到 completed`, snapshotId: j.snapshotId }, safeMode: false, unresolved: false };
176
+ }
177
+
178
+ // 回滚进行中(ROLLING_BACK)或已部分补偿(entryDone 非空):**绝不判 RECOVERED**
179
+ // (P0-B:崩溃在回滚中 = 半回滚态,判 recovered 会把破坏状态误报为成功)。
180
+ if (j.state === 'ROLLING_BACK' || Object.keys(j.rollback.entryDone ?? {}).length > 0) {
181
+ const reason = `回滚中断(${j.state === 'ROLLING_BACK' ? 'ROLLING_BACK' : `entryDone ${Object.keys(j.rollback.entryDone).join(',')}`}),需显式续跑回滚(用户确认)`;
182
+ await store.writeSafeMode(true).catch(() => undefined);
183
+ await store.appendRecoveryHistory('rollback-interrupted', { operationId, at: new Date().toISOString(), reason }).catch(() => undefined);
184
+ return { decision: { operationId, kind: 'rollback-continue', reason, snapshotId: j.snapshotId }, safeMode: true, unresolved: true };
185
+ }
186
+
187
+ // §6.5 recovery hard gate(Reviewer A P0 + 第三轮 P2 收紧):
188
+ // 置于 entryDone/回滚中断检查(:180)之后、step 指纹判定(:187)之前。
189
+ // RECOVERING +(无 recoveryVerification 或 verdict 非 MATCH/PARTIAL_MATCH)→ needs-attention,
190
+ // **绝不自动判 RECOVERED**(VERIFIED 不变量:恢复未验证不得视为完成)。
191
+ // 只有 recoveryVerification.verdict === 'MATCH' | 'PARTIAL_MATCH' 的 RECOVERING journal
192
+ // 才可转 ROLLED_BACK / RECOVERED(该终态迁移由 verify 路由以单次原子 journal update 完成)。
193
+ if (j.state === 'RECOVERING') {
194
+ const v = j.recoveryVerification;
195
+ const verified = v !== undefined && (v.verdict === 'MATCH' || v.verdict === 'PARTIAL_MATCH');
196
+ if (!verified) {
197
+ const reason = 'RECOVERING 且 recovery 未验证(无 recoveryVerification 或 verdict 非 MATCH/PARTIAL_MATCH),绝不自动 RECOVERED';
198
+ await store.writeSafeMode(true).catch(() => undefined);
199
+ await store.update(operationId, (cur) => {
200
+ let next = transitionJournalState(cur, 'NEEDS_ATTENTION');
201
+ next = { ...next, error: next.error || reason, recovery: { ...next.recovery, reason, attempts: next.recovery.attempts + 1 } };
202
+ return next;
203
+ }).catch(() => undefined);
204
+ await store.appendRecoveryHistory('recovery-unverified', { operationId, at: new Date().toISOString(), reason }).catch(() => undefined);
205
+ return { decision: { operationId, kind: 'needs-attention', reason, snapshotId: j.snapshotId }, safeMode: true, unresolved: true };
206
+ }
207
+ }
208
+
209
+ // incomplete:判定 steps
210
+ const stepIds = [...new Set([...j.plannedSteps, ...Object.keys(j.steps)])];
211
+ let anyApplied = false;
212
+ let allDoneConfirmed = true;
213
+ let anyUnprovable = false;
214
+ let anyExternalUnknown = false;
215
+ const details: string[] = [];
216
+
217
+ for (const stepId of stepIds) {
218
+ const step = j.steps[stepId];
219
+ const plannedExternal = step?.external === true;
220
+ if (step === undefined) {
221
+ // Review D P2:plannedSteps 引用悬空(steps 缺条目)→ 不可证明,绝不能经 noop 判 RECOVERED。
222
+ anyUnprovable = true;
223
+ allDoneConfirmed = false;
224
+ details.push(`planned step ${stepId} 缺少 steps 条目(损坏/篡改 journal)`);
225
+ continue;
226
+ }
227
+
228
+ if (plannedExternal) {
229
+ // 外部 step:probe 判定
230
+ const probe = await hooks.probeExternal(step);
231
+ if (probe === 'installed') { anyApplied = true; continue; }
232
+ if (probe === 'not-installed') { continue; } // 未装,无副作用
233
+ // half-installed / reverse-residue / unknown → 不可证明
234
+ anyExternalUnknown = true;
235
+ anyUnprovable = true;
236
+ allDoneConfirmed = false;
237
+ details.push(`external ${step.kind} ${step.ref}: ${probe}`);
238
+ continue;
239
+ }
240
+
241
+ // 本地 step:指纹判定
242
+ if (step.status === 'done') {
243
+ // 即使 done 也重验 afterFp(Windows rename 顺序失效)
244
+ let verdict: StepFingerprintVerdict;
245
+ try { verdict = await hooks.verifyStepFingerprint(step); } catch { verdict = 'unable'; }
246
+ if (verdict === 'after-match') { anyApplied = true; continue; }
247
+ if (verdict === 'before-match') { anyApplied = false; details.push(`step ${step.ref} 未可靠完成(磁盘==before)`); allDoneConfirmed = false; anyUnprovable = true; continue; }
248
+ anyUnprovable = true; allDoneConfirmed = false;
249
+ details.push(`step ${step.ref} 指纹 ${verdict}`);
250
+ } else {
251
+ // planned/attention/failed → 未完成
252
+ allDoneConfirmed = false;
253
+ if (step.status === 'attention' || step.status === 'failed') { anyUnprovable = true; details.push(`step ${step.ref} ${step.status}`); }
254
+ else { // planned:指纹判定 side effect 是否发生
255
+ let verdict: StepFingerprintVerdict;
256
+ try { verdict = await hooks.verifyStepFingerprint(step); } catch { verdict = 'unable'; }
257
+ if (verdict === 'after-match') { anyApplied = true; }
258
+ else if (verdict === 'before-match') { /* 未应用,安全 */ }
259
+ else { anyUnprovable = true; anyApplied = anyApplied || verdict === 'none'; details.push(`step ${step.ref} ${verdict}`); }
260
+ }
261
+ }
262
+ }
263
+
264
+ const snapshotExists = await hooks.snapshotExists(j.snapshotId, {
265
+ operationId: j.operationId,
266
+ ownerInstanceId: j.ownerInstanceId,
267
+ environmentFingerprint: j.environmentFingerprint,
268
+ });
269
+
270
+ // F20 修复:空 steps(opaque intent journal)不得因 [].every() 真空真而判 RECOVERED。
271
+ // 若 mutation 是否执行无法证明 → NEEDS_ATTENTION;若有 trusted bound snapshot → rollback-recommended。
272
+ if (stepIds.length === 0) {
273
+ // CREATED / SNAPSHOT_CREATED:mutation 尚未开始 → 安全 no-op
274
+ if (j.state === 'CREATED' || j.state === 'SNAPSHOT_CREATED') {
275
+ await store.update(operationId, (cur) => {
276
+ let next = transitionJournalState(cur, 'RECOVERED');
277
+ next = { ...next, recovery: { ...next.recovery, attemptedAt: new Date().toISOString(), outcome: 'RECOVERED', reason: 'no mutation started (empty steps, pre-APPLYING)', attempts: next.recovery.attempts + 1 } };
278
+ return next;
279
+ });
280
+ await store.moveToCompleted(operationId).catch(() => undefined);
281
+ await store.appendRecoveryHistory('noop', { operationId, at: new Date().toISOString(), reason: 'no mutation started' }).catch(() => undefined);
282
+ return { decision: { operationId, kind: 'noop', reason: 'mutation 未开始,安全完成', snapshotId: j.snapshotId }, safeMode: false, unresolved: false };
283
+ }
284
+ // APPLYING(或其它非 terminal):mutation 可能已开始,无法证明
285
+ if (j.snapshotId !== null && snapshotExists) {
286
+ // 有 trusted bound snapshot → rollback-recommended(需用户确认)
287
+ await store.writeSafeMode(true).catch(() => undefined);
288
+ const reason = 'opaque APPLYING(空 steps)且 mutation 可能已开始,有 trusted snapshot 可回滚(需用户确认)';
289
+ await store.update(operationId, (cur) => {
290
+ let next = transitionJournalState(cur, 'NEEDS_ATTENTION');
291
+ next = { ...next, error: next.error || reason, recovery: { ...next.recovery, reason, attempts: next.recovery.attempts + 1 } };
292
+ return next;
293
+ }).catch(() => undefined);
294
+ await store.appendRecoveryHistory('needs-attention', { operationId, at: new Date().toISOString(), reason }).catch(() => undefined);
295
+ return { decision: { operationId, kind: 'rollback-recommended', reason, snapshotId: j.snapshotId }, safeMode: true, unresolved: true };
296
+ }
297
+ // 无 trusted snapshot → NEEDS_ATTENTION(SAFE MODE)
298
+ await store.writeSafeMode(true).catch(() => undefined);
299
+ const reason = 'opaque APPLYING(空 steps)且 mutation 可能已开始,无 trusted snapshot 可回滚';
300
+ await store.update(operationId, (cur) => {
301
+ let next = transitionJournalState(cur, 'NEEDS_ATTENTION');
302
+ next = { ...next, error: next.error || reason, recovery: { ...next.recovery, reason, attempts: next.recovery.attempts + 1 } };
303
+ return next;
304
+ }).catch(() => undefined);
305
+ await store.appendRecoveryHistory('needs-attention', { operationId, at: new Date().toISOString(), reason }).catch(() => undefined);
306
+ return { decision: { operationId, kind: 'needs-attention', reason, snapshotId: j.snapshotId }, safeMode: true, unresolved: true };
307
+ }
308
+
309
+ // 决策
310
+ if (allDoneConfirmed && !anyUnprovable) {
311
+ // 所有 step 已 prove done → resume(RECOVERED),不重做
312
+ await store.update(operationId, (cur) => {
313
+ let next = transitionJournalState(cur, 'RECOVERED');
314
+ next = { ...next, recovery: { ...next.recovery, attemptedAt: new Date().toISOString(), outcome: 'RECOVERED', reason: 'all steps confirmed done', attempts: next.recovery.attempts + 1 } };
315
+ return next;
316
+ });
317
+ await store.moveToCompleted(operationId).catch(() => undefined);
318
+ await store.appendRecoveryHistory('recovered', { operationId, at: new Date().toISOString(), reason: 'all steps proven done' }).catch(() => undefined);
319
+ return { decision: { operationId, kind: 'recovered', reason: 'all steps proven applied', snapshotId: j.snapshotId }, safeMode: false, unresolved: false };
320
+ }
321
+
322
+ if (!anyApplied && !anyUnprovable) {
323
+ // 无任何 step 应用(全 beforeFp / all planned)+ 无外部/不可指纹 → no-op 安全丢弃
324
+ await store.update(operationId, (cur) => {
325
+ let next = transitionJournalState(cur, 'RECOVERED');
326
+ next = { ...next, recovery: { ...next.recovery, attemptedAt: new Date().toISOString(), outcome: 'RECOVERED', reason: 'no step applied', attempts: next.recovery.attempts + 1 } };
327
+ return next;
328
+ });
329
+ await store.moveToCompleted(operationId).catch(() => undefined);
330
+ await store.appendRecoveryHistory('noop', { operationId, at: new Date().toISOString(), reason: 'no step applied' }).catch(() => undefined);
331
+ return { decision: { operationId, kind: 'noop', reason: '无副作用可恢复,安全完成', snapshotId: j.snapshotId }, safeMode: false, unresolved: false };
332
+ }
333
+
334
+ if (anyUnprovable || anyExternalUnknown || !snapshotExists) {
335
+ // 不可证明 / 外部未知 / 快照缺失 → NEEDS_ATTENTION(SAFE MODE)
336
+ await store.writeSafeMode(true).catch(() => undefined);
337
+ const reason = `不可证明步骤: ${details.join('; ') || '外部副作用未知'}` + (snapshotExists ? '' : ';快照缺失');
338
+ await store.update(operationId, (cur) => {
339
+ let next = transitionJournalState(cur, 'NEEDS_ATTENTION');
340
+ next = { ...next, error: next.error || reason, recovery: { ...next.recovery, reason, attempts: next.recovery.attempts + 1 } };
341
+ return next;
342
+ }).catch(() => undefined);
343
+ await store.appendRecoveryHistory('needs-attention', { operationId, at: new Date().toISOString(), reason }).catch(() => undefined);
344
+ return { decision: { operationId, kind: 'needs-attention', reason, snapshotId: j.snapshotId }, safeMode: true, unresolved: true };
345
+ }
346
+
347
+ // 快照有效 + 已应用部分 + 无不可证明 → rollback 可安全执行(需用户确认)
348
+ await store.writeSafeMode(true).catch(() => undefined);
349
+ const reason = '部分 step 已应用,可安全回滚(需用户确认)';
350
+ return { decision: { operationId, kind: 'rollback-recommended', reason, snapshotId: j.snapshotId }, safeMode: true, unresolved: true };
351
+ }
352
+
353
+ // ---------- 执行(用户确认后) ----------
354
+
355
+ export interface RecoveryExecutorInput {
356
+ operationId: string;
357
+ action: 'rollback' | 'resume' | 'dismiss';
358
+ snapshotId: string | null;
359
+ }
360
+
361
+ /** 执行恢复(破坏性动作必须 userConfirm=true 才执行)。 */
362
+ export interface RecoveryExecutorInput {
363
+ operationId: string;
364
+ action: 'rollback' | 'resume' | 'dismiss';
365
+ snapshotId: string | null;
366
+ /**
367
+ * 恢复决策(§5.2 引擎映射):rollback-recommended → restore executor(恢复到 trusted snapshot);
368
+ * rollback-continue → rollback executor(续跑中断回滚)。缺省 = rollback-continue(兼容旧调用)。
369
+ */
370
+ decision?: 'rollback-recommended' | 'rollback-continue';
371
+ /** 宿主注入的实际回滚执行器(rollback-continue:续跑中断回滚;破坏性副作用由宿主/Coordinator 执行)。 */
372
+ performRollback?: (snapshotId: string) => Promise<{ full: boolean; failed: string[] }>;
373
+ /** 宿主注入的 restore 执行器(rollback-recommended:恢复到 trusted snapshot)。 */
374
+ performRestore?: (snapshotId: string) => Promise<{ full: boolean; failed: string[] }>;
375
+ }
376
+
377
+ export async function executeRecovery(
378
+ store: JournalStore,
379
+ input: RecoveryExecutorInput,
380
+ userConfirmed: boolean,
381
+ ): Promise<'done' | 'needs-confirmation' | 'failed'> {
382
+ const j = await store.loadActive(input.operationId);
383
+ if (j === null) return 'failed'; // 不再 active(已处理/隔离)
384
+
385
+ if (input.action === 'resume') {
386
+ // resume 不重做已完成副作用:标记 RECOVERED + move
387
+ await store.update(input.operationId, (cur) => {
388
+ let next = cur.state === 'RECOVERED' ? cur : transitionJournalState(cur, 'RECOVERED');
389
+ next = { ...next, recovery: { ...next.recovery, attemptedAt: new Date().toISOString(), outcome: 'RECOVERED', reason: 'user resume' } };
390
+ return next;
391
+ });
392
+ await store.moveToCompleted(input.operationId).catch(() => undefined);
393
+ return 'done';
394
+ }
395
+
396
+ if (input.action === 'rollback') {
397
+ if (!userConfirmed) return 'needs-confirmation'; // 普通 metadata 不是 destructive 授权
398
+ if (input.snapshotId === null) return 'failed'; // 无有效快照不可回滚
399
+ // §11.2:execute 仅接受 NEEDS_ATTENTION(confirm 后)或 RECOVERING(retry 续跑);terminal 拒绝 replay。
400
+ if (j.state !== 'NEEDS_ATTENTION' && j.state !== 'RECOVERING') return 'failed';
401
+ // §5.2 引擎映射:rollback-recommended → restore executor;rollback-continue → rollback executor。
402
+ // 不混用两套引擎;缺省 decision 走 rollback executor(兼容旧调用)。
403
+ const executor = input.decision === 'rollback-recommended' ? input.performRestore : input.performRollback;
404
+ if (executor === undefined) return 'failed';
405
+ // 真正开始时:NEEDS_ATTENTION → RECOVERING(§4.2/§7.1)。recovery 全程保持 RECOVERING,
406
+ // **不引入 ROLLING_BACK**(ALLOWED_TRANSITIONS['RECOVERING'] 不含 ROLLING_BACK)。
407
+ // retry 续跑时 journal 已 RECOVERING → 幂等保持。
408
+ await store.update(input.operationId, (cur) => {
409
+ if (cur.state === 'RECOVERING') return cur;
410
+ if (cur.state === 'NEEDS_ATTENTION') return transitionJournalState(cur, 'RECOVERING');
411
+ return cur; // 其它状态(terminal 等)不迁移
412
+ }).catch(() => undefined);
413
+ const report = await executor(input.snapshotId);
414
+ // 执行完成:保持 RECOVERING(记录 rollback 报告;终态迁移 + verification 写入由 verify 路由
415
+ // 以单次原子 journal update 完成,见 §6.5)。此处绝不自行转 ROLLED_BACK。
416
+ await store.update(input.operationId, (cur) => {
417
+ return { ...cur, rollback: { ...cur.rollback, full: report.full, failed: report.failed } };
418
+ }).catch(() => undefined);
419
+ return 'done';
420
+ }
421
+
422
+ // dismiss(quarantine & dismiss):移出 active,不视为恢复(用户显式放弃)
423
+ if (!userConfirmed) return 'needs-confirmation';
424
+ await store.quarantine(input.operationId, 'user dismissed').catch(() => undefined);
425
+ return 'done';
426
+ }
427
+
428
+ // ---------- recovery decision 重算(§5.4) ----------
429
+
430
+ /**
431
+ * 对 terminal `NEEDS_ATTENTION` journal 重算 recovery 决策(§5.4,Reviewer A 第二轮 P1-2)。
432
+ *
433
+ * `reconcileActive` 对 terminal `NEEDS_ATTENTION` journal 恒返回 `needs-attention`
434
+ * (`NEEDS_ATTENTION` 是 terminal,`rollback-recommended`/`rollback-continue` 只在转换进
435
+ * NEEDS_ATTENTION 的当次 pass 出现一次)。`GET /recovery/status` 用本函数重算,区分
436
+ * `rollback-recommended` / `rollback-continue` / `needs-attention`。
437
+ *
438
+ * **只读**:不修改 journal,不写 SAFE MODE,不写 recovery-history。
439
+ */
440
+ export async function recomputeRecoveryDecision(
441
+ j: OperationJournal,
442
+ hooks: Pick<ReconcileProbeHooks, 'snapshotExists'>,
443
+ ): Promise<'rollback-continue' | 'rollback-recommended' | 'needs-attention'> {
444
+ // 回滚中断(entryDone 非空)→ 续跑中断回滚
445
+ if (Object.keys(j.rollback.entryDone ?? {}).length > 0) return 'rollback-continue';
446
+ // 有合法且存在的 trusted bound snapshot → 可恢复到快照
447
+ if (j.snapshotId !== null && j.snapshotId !== '') {
448
+ const exists = await hooks.snapshotExists(j.snapshotId, {
449
+ operationId: j.operationId,
450
+ ownerInstanceId: j.ownerInstanceId,
451
+ environmentFingerprint: j.environmentFingerprint,
452
+ });
453
+ if (exists) return 'rollback-recommended';
454
+ }
455
+ return 'needs-attention';
456
+ }
457
+
458
+ // ---------- startup 只读判定(不自动 recover stale lock) ----------
459
+
460
+ export interface StartupInspection {
461
+ safeModeRequired: boolean;
462
+ recoveryRequired: boolean;
463
+ decisions: ReconcileDecision[];
464
+ unresolved: string[];
465
+ }
466
+
467
+ /**
468
+ * Startup 只读事务状态 + 锁状态扫描(Rev 3 P1-NEW-2):
469
+ * 只检测、只读;**不自动 recoverStaleLock、不自动 create journal**。
470
+ * lockState: 'LOCKED' | 'STALE_LOCK_DETECTED' | 'UNKNOWN_STATE' | 'FREE'
471
+ */
472
+ export async function inspectStartup(
473
+ store: JournalStore,
474
+ hooks: ReconcileProbeHooks,
475
+ env: ReconcileEnv,
476
+ opts: ReconcileOptions,
477
+ lockState: 'LOCKED' | 'STALE_LOCK_DETECTED' | 'UNKNOWN_STATE' | 'FREE',
478
+ ): Promise<StartupInspection> {
479
+ // 锁为 LOCKED(fresh heartbeat,活跃 owner)→ 任何 incomplete journal 皆视为 live op,不 reconcile/quarantine/move;
480
+ // 仅 STALE / UNKNOWN / FREE 时才对 journal 做 recover 判定(修 isLiveOwner 并发误判,§18)。
481
+ // 注意:此处用 `||`(不能 `&&`——env.isLiveOwner 在保守 hooks 下恒 false,`LOCKED && false` 会让 live 分支永不到达)。
482
+ const envWithLive = {
483
+ ...env,
484
+ isLiveOwner: async (j: OperationJournal) => lockState === 'LOCKED' || (await env.isLiveOwner(j)),
485
+ };
486
+ const outcome = await reconcileActive(store, hooks, envWithLive, { ...opts, organizeTerminal: true });
487
+ const hasIncomplete = outcome.unresolved.length > 0;
488
+
489
+ let recoveryRequired = false;
490
+ if (hasIncomplete && (lockState === 'STALE_LOCK_DETECTED' || lockState === 'UNKNOWN_STATE')) {
491
+ recoveryRequired = true;
492
+ await store.writeSafeMode(true).catch(() => undefined);
493
+ }
494
+ // 读 durable SAFE MODE 标记(跨重启保持阻断):即使 active 已规整,标记在则仍须阻断
495
+ const durableSafe = await store.readSafeMode().catch(() => false);
496
+ const safeModeRequired = outcome.safeModeRequired || recoveryRequired || durableSafe;
497
+ return { safeModeRequired, recoveryRequired, decisions: outcome.decisions, unresolved: outcome.unresolved };
498
+ }
@@ -0,0 +1,145 @@
1
+ /**
2
+ * RecoveryOrchestrator 测试(issue #31 聚焦)。
3
+ *
4
+ * 背景:残留环境锁**不是** journal —— 进程死在 `op=autosync` 期间时 `journalId: null`、
5
+ * `transactions/active/` 为空,因此 status() 的 incidents 恒为 [],「事故恢复」面板对纯锁
6
+ * 残留恒空,而 423 文案却让用户去那个面板处理。本测试锁定三件事:
7
+ * ① status() 必须附带环境锁分类,让面板能显示可执行的锁事项;
8
+ * ② 锁分类**只上报 state/attention**,绝不把 owner pid/op 等内部诊断放进响应体;
9
+ * ③ recoverStaleLock() 必须要求显式确认,且**绝不**在未证明 stale 时谎称成功。
10
+ */
11
+ import test from 'node:test';
12
+ import assert from 'node:assert/strict';
13
+ import fs from 'node:fs/promises';
14
+ import os from 'node:os';
15
+ import path from 'node:path';
16
+
17
+ import { createRecoveryOrchestrator } from './recovery-orchestrator.ts';
18
+ import type { RecoveryOrchestratorDeps } from './recovery-orchestrator.ts';
19
+ import { JournalStore } from './journal.ts';
20
+ import { RunRegistry } from './run-registry.ts';
21
+ import { nullLogger } from '../utils/logger.ts';
22
+ import { zhMsg } from './messages.ts';
23
+ import type { HostContext } from './types.ts';
24
+
25
+ /** 记录注入依赖的调用次数(断言「拒绝时不得调用回收」用)。 */
26
+ interface RecoverSpy {
27
+ calls: number;
28
+ result: { ok: boolean; removed: boolean; state: string; detail?: string };
29
+ }
30
+
31
+ async function makeOrchestrator(opts: {
32
+ lockState?: string;
33
+ lockDetail?: string;
34
+ lockThrows?: boolean;
35
+ recoverResult?: { ok: boolean; removed: boolean; state: string; detail?: string };
36
+ } = {}): Promise<{ orchestrator: ReturnType<typeof createRecoveryOrchestrator>; spy: RecoverSpy }> {
37
+ const dir = await fs.mkdtemp(path.join(os.tmpdir(), 'dsh-recovery-orch-'));
38
+ const spy: RecoverSpy = {
39
+ calls: 0,
40
+ result: opts.recoverResult ?? { ok: true, removed: true, state: 'STALE_LOCK_DETECTED', detail: '已移除 stale ownership (op=autosync, pid=24140)' },
41
+ };
42
+ const deps: RecoveryOrchestratorDeps = {
43
+ store: new JournalStore({ transactionsDir: path.join(dir, 'transactions') }),
44
+ runs: new RunRegistry(),
45
+ snapshotsDir: path.join(dir, 'snapshots'),
46
+ host: { log: nullLogger() } as unknown as HostContext,
47
+ msg: zhMsg,
48
+ snapshotExists: async () => false,
49
+ getEnvironmentFingerprint: () => 'fp-test',
50
+ clearSafeMode: async () => undefined,
51
+ inspectLockState: async () => {
52
+ if (opts.lockThrows === true) throw new Error('probe failed');
53
+ return { state: opts.lockState ?? 'FREE', ...(opts.lockDetail !== undefined ? { detail: opts.lockDetail } : {}) };
54
+ },
55
+ recoverStaleLock: async () => {
56
+ spy.calls += 1;
57
+ return spy.result;
58
+ },
59
+ };
60
+ return { orchestrator: createRecoveryOrchestrator(deps), spy };
61
+ }
62
+
63
+ // ---------- ① 纯锁残留场景:incidents 为空,但锁事项必须可见 ----------
64
+
65
+ test('status:无 journal(纯残留锁)时 incidents 为空,但仍上报锁分类供面板显示', async () => {
66
+ const { orchestrator } = await makeOrchestrator({ lockState: 'STALE_LOCK_DETECTED' });
67
+ const r = await orchestrator.status();
68
+ assert.equal(r.status, 200);
69
+ assert.deepEqual(r.body['incidents'], [], '残留锁不是 journal → incidents 恒空(#31 根因 C)');
70
+ assert.deepEqual(r.body['lock'], { state: 'STALE_LOCK_DETECTED', attention: true });
71
+ });
72
+
73
+ test('status:LOCKED(另一任务活跃持有)→ attention=false,不催用户回收', async () => {
74
+ const { orchestrator } = await makeOrchestrator({ lockState: 'LOCKED' });
75
+ const r = await orchestrator.status();
76
+ assert.deepEqual(r.body['lock'], { state: 'LOCKED', attention: false });
77
+ });
78
+
79
+ test('status:UNKNOWN_STATE(无法判定)→ attention=true', async () => {
80
+ const { orchestrator } = await makeOrchestrator({ lockState: 'UNKNOWN_STATE' });
81
+ const r = await orchestrator.status();
82
+ assert.deepEqual(r.body['lock'], { state: 'UNKNOWN_STATE', attention: true });
83
+ });
84
+
85
+ test('status:锁探测抛错 → 不阻断 status,保守上报 UNKNOWN_STATE/attention', async () => {
86
+ const { orchestrator } = await makeOrchestrator({ lockThrows: true });
87
+ const r = await orchestrator.status();
88
+ assert.equal(r.status, 200, '探测失败不得让整个恢复面板加载失败');
89
+ assert.deepEqual(r.body['lock'], { state: 'UNKNOWN_STATE', attention: true });
90
+ });
91
+
92
+ // ---------- ② 响应体不得含内部诊断(pid/op) ----------
93
+
94
+ test('status:锁分类只上报 state/attention,绝不泄漏 owner pid/op(detail 只进日志)', async () => {
95
+ const { orchestrator } = await makeOrchestrator({
96
+ lockState: 'STALE_LOCK_DETECTED',
97
+ lockDetail: 'owner pid=24140 确证不存在 (heartbeat expired)',
98
+ });
99
+ const r = await orchestrator.status();
100
+ const serialized = JSON.stringify(r.body);
101
+ assert.equal(serialized.includes('24140'), false, 'owner pid 不得进入响应体');
102
+ assert.equal(serialized.includes('heartbeat'), false, '判定依据细节不得进入响应体');
103
+ assert.deepEqual(r.body['lock'], { state: 'STALE_LOCK_DETECTED', attention: true });
104
+ });
105
+
106
+ // ---------- ③ 显式回收:需确认、不谎称成功 ----------
107
+
108
+ test('recoverStaleLock:未携带 userConfirmed=true → 400 且不调用回收', async () => {
109
+ const { orchestrator, spy } = await makeOrchestrator();
110
+ const r = await orchestrator.recoverStaleLock(false);
111
+ assert.equal(r.status, 400);
112
+ assert.equal(spy.calls, 0, '未确认不得触碰锁文件');
113
+ });
114
+
115
+ test('recoverStaleLock:确认后成功 → ok/removed,且响应体不含内部诊断', async () => {
116
+ const { orchestrator, spy } = await makeOrchestrator();
117
+ const r = await orchestrator.recoverStaleLock(true);
118
+ assert.equal(spy.calls, 1);
119
+ assert.equal(r.status, 200);
120
+ assert.equal(r.body['ok'], true);
121
+ assert.equal(r.body['removed'], true);
122
+ assert.equal(JSON.stringify(r.body).includes('24140'), false, 'pid 只进日志');
123
+ });
124
+
125
+ test('recoverStaleLock:未被判定为 stale → ok=false(绝不谎称成功),且不改动任何东西', async () => {
126
+ const { orchestrator, spy } = await makeOrchestrator({
127
+ recoverResult: { ok: false, removed: false, state: 'LOCKED', detail: '非 stale,拒绝 recovery' },
128
+ });
129
+ const r = await orchestrator.recoverStaleLock(true);
130
+ assert.equal(r.status, 200, '拒绝是正常结果而非服务错误');
131
+ assert.equal(r.body['ok'], false);
132
+ assert.equal(r.body['removed'], false);
133
+ assert.equal(r.body['state'], 'LOCKED');
134
+ assert.equal(spy.calls, 1);
135
+ });
136
+
137
+ // ---------- 源码守卫:回收路由的接线形态(#31 的关键不变量) ----------
138
+
139
+ /**
140
+ * 为什么必须守卫:若要回收的正是那把挡住 acquire 的残留锁,把回收路由改成
141
+ * `runWithMutationLock`/`withMutationGate` 包裹后,acquire 必然返回 STALE_LOCK_DETECTED
142
+ * → 抛 423 → **回收永远无法执行**(正是本 issue 报告的那类「入口存在但结构上不可达」)。
143
+ * 同时锁分支必须排在 :operationId 解析之前——'lock' 不是 UUID,否则会被 400 挡掉。
144
+ * 按文本解析源码前先归一化行尾(Windows 工作区 CRLF / CI LF),否则守卫只在一边通过。
145
+ */