ws-spec-kit 0.1.0-beta.0

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 (353) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +79 -0
  3. package/dist/adapters/cli/output.d.ts +15 -0
  4. package/dist/adapters/cli/output.js +21 -0
  5. package/dist/adapters/cli/output.js.map +1 -0
  6. package/dist/adapters/cli/workflow.d.ts +25 -0
  7. package/dist/adapters/cli/workflow.js +131 -0
  8. package/dist/adapters/cli/workflow.js.map +1 -0
  9. package/dist/adapters/connectors/git-native.d.ts +23 -0
  10. package/dist/adapters/connectors/git-native.js +521 -0
  11. package/dist/adapters/connectors/git-native.js.map +1 -0
  12. package/dist/adapters/connectors/github-cli.d.ts +23 -0
  13. package/dist/adapters/connectors/github-cli.js +213 -0
  14. package/dist/adapters/connectors/github-cli.js.map +1 -0
  15. package/dist/adapters/connectors/gitlab-cli.d.ts +23 -0
  16. package/dist/adapters/connectors/gitlab-cli.js +199 -0
  17. package/dist/adapters/connectors/gitlab-cli.js.map +1 -0
  18. package/dist/adapters/connectors/lark-cli.d.ts +24 -0
  19. package/dist/adapters/connectors/lark-cli.js +296 -0
  20. package/dist/adapters/connectors/lark-cli.js.map +1 -0
  21. package/dist/adapters/process/redaction.d.ts +2 -0
  22. package/dist/adapters/process/redaction.js +51 -0
  23. package/dist/adapters/process/redaction.js.map +1 -0
  24. package/dist/adapters/process/spawn-json.d.ts +37 -0
  25. package/dist/adapters/process/spawn-json.js +312 -0
  26. package/dist/adapters/process/spawn-json.js.map +1 -0
  27. package/dist/adapters/skills/claude.d.ts +1 -0
  28. package/dist/adapters/skills/claude.js +3 -0
  29. package/dist/adapters/skills/claude.js.map +1 -0
  30. package/dist/adapters/skills/codex.d.ts +1 -0
  31. package/dist/adapters/skills/codex.js +3 -0
  32. package/dist/adapters/skills/codex.js.map +1 -0
  33. package/dist/adapters/skills/cursor.d.ts +1 -0
  34. package/dist/adapters/skills/cursor.js +3 -0
  35. package/dist/adapters/skills/cursor.js.map +1 -0
  36. package/dist/adapters/skills/generic.d.ts +1 -0
  37. package/dist/adapters/skills/generic.js +2 -0
  38. package/dist/adapters/skills/generic.js.map +1 -0
  39. package/dist/adapters/skills/install.d.ts +28 -0
  40. package/dist/adapters/skills/install.js +571 -0
  41. package/dist/adapters/skills/install.js.map +1 -0
  42. package/dist/application/acquire.d.ts +60 -0
  43. package/dist/application/acquire.js +964 -0
  44. package/dist/application/acquire.js.map +1 -0
  45. package/dist/application/application.d.ts +21 -0
  46. package/dist/application/application.js +31 -0
  47. package/dist/application/application.js.map +1 -0
  48. package/dist/application/artifact-reader.d.ts +1 -0
  49. package/dist/application/artifact-reader.js +150 -0
  50. package/dist/application/artifact-reader.js.map +1 -0
  51. package/dist/application/artifact-writer.d.ts +1 -0
  52. package/dist/application/artifact-writer.js +268 -0
  53. package/dist/application/artifact-writer.js.map +1 -0
  54. package/dist/application/artifact.d.ts +28 -0
  55. package/dist/application/artifact.js +523 -0
  56. package/dist/application/artifact.js.map +1 -0
  57. package/dist/application/decide.d.ts +12 -0
  58. package/dist/application/decide.js +168 -0
  59. package/dist/application/decide.js.map +1 -0
  60. package/dist/application/doctor-connectors.d.ts +34 -0
  61. package/dist/application/doctor-connectors.js +99 -0
  62. package/dist/application/doctor-connectors.js.map +1 -0
  63. package/dist/application/external-action.d.ts +164 -0
  64. package/dist/application/external-action.js +670 -0
  65. package/dist/application/external-action.js.map +1 -0
  66. package/dist/application/initial-stages.d.ts +3 -0
  67. package/dist/application/initial-stages.js +7 -0
  68. package/dist/application/initial-stages.js.map +1 -0
  69. package/dist/application/inspect.d.ts +2 -0
  70. package/dist/application/inspect.js +31 -0
  71. package/dist/application/inspect.js.map +1 -0
  72. package/dist/application/profile.d.ts +32 -0
  73. package/dist/application/profile.js +70 -0
  74. package/dist/application/profile.js.map +1 -0
  75. package/dist/application/snapshot.d.ts +75 -0
  76. package/dist/application/snapshot.js +338 -0
  77. package/dist/application/snapshot.js.map +1 -0
  78. package/dist/application/start.d.ts +43 -0
  79. package/dist/application/start.js +360 -0
  80. package/dist/application/start.js.map +1 -0
  81. package/dist/application/state.d.ts +17 -0
  82. package/dist/application/state.js +94 -0
  83. package/dist/application/state.js.map +1 -0
  84. package/dist/application/submit.d.ts +18 -0
  85. package/dist/application/submit.js +1090 -0
  86. package/dist/application/submit.js.map +1 -0
  87. package/dist/cli/commands/core.d.ts +2 -0
  88. package/dist/cli/commands/core.js +205 -0
  89. package/dist/cli/commands/core.js.map +1 -0
  90. package/dist/cli/commands/public-contract.d.ts +12 -0
  91. package/dist/cli/commands/public-contract.js +47 -0
  92. package/dist/cli/commands/public-contract.js.map +1 -0
  93. package/dist/cli/main.d.ts +2 -0
  94. package/dist/cli/main.js +23 -0
  95. package/dist/cli/main.js.map +1 -0
  96. package/dist/domain/artifacts.d.ts +52 -0
  97. package/dist/domain/artifacts.js +241 -0
  98. package/dist/domain/artifacts.js.map +1 -0
  99. package/dist/domain/digests.d.ts +11 -0
  100. package/dist/domain/digests.js +106 -0
  101. package/dist/domain/digests.js.map +1 -0
  102. package/dist/domain/external-receipt.d.ts +48 -0
  103. package/dist/domain/external-receipt.js +138 -0
  104. package/dist/domain/external-receipt.js.map +1 -0
  105. package/dist/domain/ids.d.ts +7 -0
  106. package/dist/domain/ids.js +5 -0
  107. package/dist/domain/ids.js.map +1 -0
  108. package/dist/domain/repository-path.d.ts +3 -0
  109. package/dist/domain/repository-path.js +87 -0
  110. package/dist/domain/repository-path.js.map +1 -0
  111. package/dist/domain/states.d.ts +38 -0
  112. package/dist/domain/states.js +69 -0
  113. package/dist/domain/states.js.map +1 -0
  114. package/dist/domain/work-package-identity.d.ts +2 -0
  115. package/dist/domain/work-package-identity.js +10 -0
  116. package/dist/domain/work-package-identity.js.map +1 -0
  117. package/dist/domain/workflow.d.ts +58 -0
  118. package/dist/domain/workflow.js +2 -0
  119. package/dist/domain/workflow.js.map +1 -0
  120. package/dist/engine/actor-roles.d.ts +15 -0
  121. package/dist/engine/actor-roles.js +53 -0
  122. package/dist/engine/actor-roles.js.map +1 -0
  123. package/dist/engine/approvals.d.ts +44 -0
  124. package/dist/engine/approvals.js +239 -0
  125. package/dist/engine/approvals.js.map +1 -0
  126. package/dist/engine/archive.d.ts +24 -0
  127. package/dist/engine/archive.js +442 -0
  128. package/dist/engine/archive.js.map +1 -0
  129. package/dist/engine/compiler.d.ts +25 -0
  130. package/dist/engine/compiler.js +828 -0
  131. package/dist/engine/compiler.js.map +1 -0
  132. package/dist/engine/control/condition.d.ts +10 -0
  133. package/dist/engine/control/condition.js +49 -0
  134. package/dist/engine/control/condition.js.map +1 -0
  135. package/dist/engine/control/loop.d.ts +23 -0
  136. package/dist/engine/control/loop.js +96 -0
  137. package/dist/engine/control/loop.js.map +1 -0
  138. package/dist/engine/control/retry.d.ts +15 -0
  139. package/dist/engine/control/retry.js +54 -0
  140. package/dist/engine/control/retry.js.map +1 -0
  141. package/dist/engine/docs-integrity.d.ts +13 -0
  142. package/dist/engine/docs-integrity.js +34 -0
  143. package/dist/engine/docs-integrity.js.map +1 -0
  144. package/dist/engine/expressions/ast.d.ts +18 -0
  145. package/dist/engine/expressions/ast.js +9 -0
  146. package/dist/engine/expressions/ast.js.map +1 -0
  147. package/dist/engine/expressions/evaluate.d.ts +7 -0
  148. package/dist/engine/expressions/evaluate.js +47 -0
  149. package/dist/engine/expressions/evaluate.js.map +1 -0
  150. package/dist/engine/expressions/parser.d.ts +2 -0
  151. package/dist/engine/expressions/parser.js +173 -0
  152. package/dist/engine/expressions/parser.js.map +1 -0
  153. package/dist/engine/external-effects/authorization.d.ts +146 -0
  154. package/dist/engine/external-effects/authorization.js +222 -0
  155. package/dist/engine/external-effects/authorization.js.map +1 -0
  156. package/dist/engine/external-effects/idempotency.d.ts +14 -0
  157. package/dist/engine/external-effects/idempotency.js +26 -0
  158. package/dist/engine/external-effects/idempotency.js.map +1 -0
  159. package/dist/engine/external-effects/reconciliation.d.ts +11 -0
  160. package/dist/engine/external-effects/reconciliation.js +18 -0
  161. package/dist/engine/external-effects/reconciliation.js.map +1 -0
  162. package/dist/engine/results.d.ts +14 -0
  163. package/dist/engine/results.js +88 -0
  164. package/dist/engine/results.js.map +1 -0
  165. package/dist/engine/scheduler.d.ts +60 -0
  166. package/dist/engine/scheduler.js +225 -0
  167. package/dist/engine/scheduler.js.map +1 -0
  168. package/dist/engine/tdd/green-gate.d.ts +6 -0
  169. package/dist/engine/tdd/green-gate.js +72 -0
  170. package/dist/engine/tdd/green-gate.js.map +1 -0
  171. package/dist/engine/tdd/red-gate.d.ts +30 -0
  172. package/dist/engine/tdd/red-gate.js +531 -0
  173. package/dist/engine/tdd/red-gate.js.map +1 -0
  174. package/dist/engine/tdd/types.d.ts +89 -0
  175. package/dist/engine/tdd/types.js +38 -0
  176. package/dist/engine/tdd/types.js.map +1 -0
  177. package/dist/engine/verification.d.ts +56 -0
  178. package/dist/engine/verification.js +196 -0
  179. package/dist/engine/verification.js.map +1 -0
  180. package/dist/policy/profile.d.ts +24 -0
  181. package/dist/policy/profile.js +23 -0
  182. package/dist/policy/profile.js.map +1 -0
  183. package/dist/policy/risk.d.ts +33 -0
  184. package/dist/policy/risk.js +138 -0
  185. package/dist/policy/risk.js.map +1 -0
  186. package/dist/protocol/application.d.ts +196 -0
  187. package/dist/protocol/application.js +2 -0
  188. package/dist/protocol/application.js.map +1 -0
  189. package/dist/protocol/public-contract.d.ts +81 -0
  190. package/dist/protocol/public-contract.js +231 -0
  191. package/dist/protocol/public-contract.js.map +1 -0
  192. package/dist/protocol/work-package.d.ts +55 -0
  193. package/dist/protocol/work-package.js +2 -0
  194. package/dist/protocol/work-package.js.map +1 -0
  195. package/dist/registry/connectors/external-executor.d.ts +4 -0
  196. package/dist/registry/connectors/external-executor.js +249 -0
  197. package/dist/registry/connectors/external-executor.js.map +1 -0
  198. package/dist/registry/connectors/feishu-document.d.ts +21 -0
  199. package/dist/registry/connectors/feishu-document.js +197 -0
  200. package/dist/registry/connectors/feishu-document.js.map +1 -0
  201. package/dist/registry/connectors/git-commit.d.ts +32 -0
  202. package/dist/registry/connectors/git-commit.js +122 -0
  203. package/dist/registry/connectors/git-commit.js.map +1 -0
  204. package/dist/registry/connectors/issue.d.ts +68 -0
  205. package/dist/registry/connectors/issue.js +301 -0
  206. package/dist/registry/connectors/issue.js.map +1 -0
  207. package/dist/registry/connectors/knowledge-publish.d.ts +30 -0
  208. package/dist/registry/connectors/knowledge-publish.js +157 -0
  209. package/dist/registry/connectors/knowledge-publish.js.map +1 -0
  210. package/dist/registry/connectors/local-requirement.d.ts +19 -0
  211. package/dist/registry/connectors/local-requirement.js +177 -0
  212. package/dist/registry/connectors/local-requirement.js.map +1 -0
  213. package/dist/registry/connectors/manifest.d.ts +6 -0
  214. package/dist/registry/connectors/manifest.js +164 -0
  215. package/dist/registry/connectors/manifest.js.map +1 -0
  216. package/dist/registry/connectors/registry.d.ts +11 -0
  217. package/dist/registry/connectors/registry.js +34 -0
  218. package/dist/registry/connectors/registry.js.map +1 -0
  219. package/dist/registry/connectors/requirement-source.d.ts +46 -0
  220. package/dist/registry/connectors/requirement-source.js +513 -0
  221. package/dist/registry/connectors/requirement-source.js.map +1 -0
  222. package/dist/registry/connectors/runtime.d.ts +38 -0
  223. package/dist/registry/connectors/runtime.js +127 -0
  224. package/dist/registry/connectors/runtime.js.map +1 -0
  225. package/dist/registry/connectors/secret-detector.d.ts +19 -0
  226. package/dist/registry/connectors/secret-detector.js +85 -0
  227. package/dist/registry/connectors/secret-detector.js.map +1 -0
  228. package/dist/registry/connectors/semver.d.ts +8 -0
  229. package/dist/registry/connectors/semver.js +57 -0
  230. package/dist/registry/connectors/semver.js.map +1 -0
  231. package/dist/registry/connectors/types.d.ts +45 -0
  232. package/dist/registry/connectors/types.js +2 -0
  233. package/dist/registry/connectors/types.js.map +1 -0
  234. package/dist/registry/executors/registry.d.ts +15 -0
  235. package/dist/registry/executors/registry.js +68 -0
  236. package/dist/registry/executors/registry.js.map +1 -0
  237. package/dist/registry/executors/types.d.ts +19 -0
  238. package/dist/registry/executors/types.js +2 -0
  239. package/dist/registry/executors/types.js.map +1 -0
  240. package/dist/registry/skills/lock.d.ts +3 -0
  241. package/dist/registry/skills/lock.js +278 -0
  242. package/dist/registry/skills/lock.js.map +1 -0
  243. package/dist/registry/skills/resolver.d.ts +16 -0
  244. package/dist/registry/skills/resolver.js +353 -0
  245. package/dist/registry/skills/resolver.js.map +1 -0
  246. package/dist/registry/skills/types.d.ts +75 -0
  247. package/dist/registry/skills/types.js +9 -0
  248. package/dist/registry/skills/types.js.map +1 -0
  249. package/dist/resources/catalog.d.ts +20 -0
  250. package/dist/resources/catalog.js +111 -0
  251. package/dist/resources/catalog.js.map +1 -0
  252. package/dist/schemas/definitions.d.ts +5 -0
  253. package/dist/schemas/definitions.js +958 -0
  254. package/dist/schemas/definitions.js.map +1 -0
  255. package/dist/schemas/generate.d.ts +1 -0
  256. package/dist/schemas/generate.js +26 -0
  257. package/dist/schemas/generate.js.map +1 -0
  258. package/dist/schemas/index.d.ts +11 -0
  259. package/dist/schemas/index.js +54 -0
  260. package/dist/schemas/index.js.map +1 -0
  261. package/dist/storage/control-plane.d.ts +136 -0
  262. package/dist/storage/control-plane.js +688 -0
  263. package/dist/storage/control-plane.js.map +1 -0
  264. package/dist/storage/events.d.ts +36 -0
  265. package/dist/storage/events.js +185 -0
  266. package/dist/storage/events.js.map +1 -0
  267. package/dist/storage/external-action-payload.d.ts +33 -0
  268. package/dist/storage/external-action-payload.js +190 -0
  269. package/dist/storage/external-action-payload.js.map +1 -0
  270. package/dist/storage/files.d.ts +13 -0
  271. package/dist/storage/files.js +45 -0
  272. package/dist/storage/files.js.map +1 -0
  273. package/dist/storage/git.d.ts +4 -0
  274. package/dist/storage/git.js +21 -0
  275. package/dist/storage/git.js.map +1 -0
  276. package/dist/storage/project-config.d.ts +11 -0
  277. package/dist/storage/project-config.js +60 -0
  278. package/dist/storage/project-config.js.map +1 -0
  279. package/dist/storage/repository.d.ts +14 -0
  280. package/dist/storage/repository.js +151 -0
  281. package/dist/storage/repository.js.map +1 -0
  282. package/dist/storage/work-items.d.ts +77 -0
  283. package/dist/storage/work-items.js +373 -0
  284. package/dist/storage/work-items.js.map +1 -0
  285. package/dist/storage/workflow-trust.d.ts +38 -0
  286. package/dist/storage/workflow-trust.js +310 -0
  287. package/dist/storage/workflow-trust.js.map +1 -0
  288. package/dist/workflow-package/loader.d.ts +12 -0
  289. package/dist/workflow-package/loader.js +249 -0
  290. package/dist/workflow-package/loader.js.map +1 -0
  291. package/dist/workflow-package/lock.d.ts +6 -0
  292. package/dist/workflow-package/lock.js +12 -0
  293. package/dist/workflow-package/lock.js.map +1 -0
  294. package/dist/workflow-package/path-boundary.d.ts +5 -0
  295. package/dist/workflow-package/path-boundary.js +38 -0
  296. package/dist/workflow-package/path-boundary.js.map +1 -0
  297. package/dist/workflow-package/trust.d.ts +27 -0
  298. package/dist/workflow-package/trust.js +61 -0
  299. package/dist/workflow-package/trust.js.map +1 -0
  300. package/dist/workflow-package/types.d.ts +176 -0
  301. package/dist/workflow-package/types.js +9 -0
  302. package/dist/workflow-package/types.js.map +1 -0
  303. package/dist/workflow-package/workflow-v1.d.ts +3 -0
  304. package/dist/workflow-package/workflow-v1.js +274 -0
  305. package/dist/workflow-package/workflow-v1.js.map +1 -0
  306. package/package.json +40 -0
  307. package/resources/catalog.yaml +37 -0
  308. package/resources/connectors/github-cli.yaml +24 -0
  309. package/resources/connectors/gitlab-cli.yaml +24 -0
  310. package/resources/connectors/lark-cli.yaml +28 -0
  311. package/resources/skills/bug-fixing/SKILL.md +16 -0
  312. package/resources/skills/code-review/SKILL.md +16 -0
  313. package/resources/skills/documentation-editing/SKILL.md +16 -0
  314. package/resources/skills/documentation-exploration/SKILL.md +16 -0
  315. package/resources/skills/documentation-review/SKILL.md +16 -0
  316. package/resources/skills/requirement-exploration/SKILL.md +16 -0
  317. package/resources/skills/solution-design/SKILL.md +16 -0
  318. package/resources/skills/task-planning/SKILL.md +16 -0
  319. package/resources/skills/tdd-implementation/SKILL.md +16 -0
  320. package/resources/workflows/documentation-delivery/manifest.yaml +8 -0
  321. package/resources/workflows/documentation-delivery/profiles/governed.yaml +23 -0
  322. package/resources/workflows/documentation-delivery/profiles/quick.yaml +23 -0
  323. package/resources/workflows/documentation-delivery/profiles/standard.yaml +21 -0
  324. package/resources/workflows/documentation-delivery/workflow.yaml +109 -0
  325. package/resources/workflows/feature-delivery/manifest.yaml +8 -0
  326. package/resources/workflows/feature-delivery/profiles/governed.yaml +27 -0
  327. package/resources/workflows/feature-delivery/profiles/quick.yaml +25 -0
  328. package/resources/workflows/feature-delivery/profiles/standard.yaml +26 -0
  329. package/resources/workflows/feature-delivery/workflow.yaml +150 -0
  330. package/schemas/builtin-agent-action-v1.schema.json +509 -0
  331. package/schemas/builtin-application-acquire-input-v1.schema.json +25 -0
  332. package/schemas/builtin-application-artifact-create-input-v1.schema.json +49 -0
  333. package/schemas/builtin-application-decision-input-v1.schema.json +284 -0
  334. package/schemas/builtin-application-inspect-input-v1.schema.json +20 -0
  335. package/schemas/builtin-application-project-config-snapshot-v1.schema.json +289 -0
  336. package/schemas/builtin-application-project-config-v1.schema.json +293 -0
  337. package/schemas/builtin-application-start-input-v1.schema.json +92 -0
  338. package/schemas/builtin-application-submit-input-v1.schema.json +145 -0
  339. package/schemas/builtin-artifact-v1.schema.json +48 -0
  340. package/schemas/builtin-evidence-v1.schema.json +65 -0
  341. package/schemas/builtin-external-action-grant-v1.schema.json +298 -0
  342. package/schemas/builtin-external-action-request-v1.schema.json +288 -0
  343. package/schemas/builtin-external-binding-v1.schema.json +59 -0
  344. package/schemas/builtin-external-receipt-v1.schema.json +109 -0
  345. package/schemas/builtin-external-write-receipt-v1.schema.json +253 -0
  346. package/schemas/builtin-source-artifact-v1.schema.json +293 -0
  347. package/schemas/builtin-submit-result-v1.schema.json +111 -0
  348. package/schemas/builtin-tdd-cycle-evidence-v1.schema.json +119 -0
  349. package/schemas/builtin-tdd-node-test-report-v1.schema.json +103 -0
  350. package/schemas/builtin-tdd-trusted-evidence-v1.schema.json +184 -0
  351. package/schemas/builtin-work-item-v1.schema.json +201 -0
  352. package/schemas/builtin-work-package-v1.schema.json +254 -0
  353. package/schemas/builtin-workflow-selection-v1.schema.json +40 -0
@@ -0,0 +1,688 @@
1
+ import { mkdir, open, readFile, realpath, rmdir } from "node:fs/promises";
2
+ import path from "node:path";
3
+ import * as canonicalizeModule from "canonicalize";
4
+ import { parse } from "yaml";
5
+ import { parseApplicationSnapshot } from "../application/snapshot.js";
6
+ import { deriveInitialStages } from "../application/initial-stages.js";
7
+ import { sha256 } from "../domain/digests.js";
8
+ import { verifySourceArtifact } from "../registry/connectors/requirement-source.js";
9
+ import { assertExternalReceipts } from "../domain/external-receipt.js";
10
+ import { parseTddCycleEvidence, parseTrustedEvidence, testAssetScopeManifest, testFileManifest, trustedTestAssetFiles } from "../engine/tdd/red-gate.js";
11
+ import { transitionStage, transitionWorkItem } from "../domain/states.js";
12
+ import { interruptedRetry } from "../engine/control/retry.js";
13
+ import { emptyRuntimeRiskSignals } from "../application/profile.js";
14
+ import { loadRepository } from "./repository.js";
15
+ import { appendEventUnlocked, EventStoreError, readEvents, recoverStaleControlPlaneLock, repairIncompleteEventTail, withControlPlaneLock } from "./events.js";
16
+ import { writeFileAtomic } from "./files.js";
17
+ import { assertExternalActionProjection } from "../engine/external-effects/authorization.js";
18
+ import { workPackageIdentityDigest } from "../domain/work-package-identity.js";
19
+ export const applicationCloseEvidenceKey = "application.close";
20
+ function assertExternalProjectionContract(stored) {
21
+ const actions = stored.externalActions;
22
+ const idempotency = stored.externalActionIdempotency;
23
+ if (!Object.hasOwn(stored, "externalActions") || actions === null || typeof actions !== "object" || Array.isArray(actions)
24
+ || !Object.hasOwn(stored, "externalActionIdempotency") || idempotency === null
25
+ || typeof idempotency !== "object" || Array.isArray(idempotency)) {
26
+ throw new ControlPlaneStorageError("WSSPEC_RUNTIME_PROJECTION_INCOMPATIBLE", "运行投影缺少 externalActions/externalActionIdempotency;当前版本不提供隐式迁移。");
27
+ }
28
+ }
29
+ export class ControlPlaneStorageError extends Error {
30
+ code;
31
+ constructor(code, message) {
32
+ super(message);
33
+ this.code = code;
34
+ this.name = "ControlPlaneStorageError";
35
+ }
36
+ }
37
+ const canonicalize = canonicalizeModule.default;
38
+ function sameCanonicalValue(left, right) {
39
+ const encoded = canonicalize(left);
40
+ return encoded !== undefined && encoded === canonicalize(right);
41
+ }
42
+ function sourceReferenceFromEvents(events, workItemId) {
43
+ const captured = events.filter((event) => event.eventType === "source.captured");
44
+ if (captured.length !== 1) {
45
+ throw new ControlPlaneStorageError("WSSPEC_SOURCE_SNAPSHOT_CHANGED", "Work Item 缺少唯一可信 Source 捕获事件。");
46
+ }
47
+ const result = captured[0].result;
48
+ const value = result !== null && typeof result === "object" && !Array.isArray(result)
49
+ ? result.value
50
+ : undefined;
51
+ if (value === null || typeof value !== "object" || Array.isArray(value)) {
52
+ throw new ControlPlaneStorageError("WSSPEC_SOURCE_SNAPSHOT_CHANGED", "Source 捕获事件引用不完整。");
53
+ }
54
+ const reference = value;
55
+ if (Object.keys(reference).sort().join("\0") !== ["artifactId", "digest", "path"].join("\0")
56
+ || typeof reference.artifactId !== "string" || typeof reference.path !== "string" || typeof reference.digest !== "string"
57
+ || !reference.path.startsWith(`.wsspec/work-items/${workItemId}/source/`)) {
58
+ throw new ControlPlaneStorageError("WSSPEC_SOURCE_SNAPSHOT_CHANGED", "Source 捕获事件引用不完整。");
59
+ }
60
+ return {
61
+ artifactType: "requirement-source",
62
+ schemaVersion: 1,
63
+ artifactId: reference.artifactId,
64
+ path: reference.path,
65
+ revision: 1,
66
+ contentHash: reference.digest,
67
+ mediaType: "application/json",
68
+ };
69
+ }
70
+ function assertedTddEvidence(projection) {
71
+ const prefix = `tdd:${projection.workItemId}:`;
72
+ const entries = Object.entries(projection.evidence).filter(([key]) => key.startsWith("tdd:"));
73
+ if (entries.length === 0)
74
+ return {};
75
+ const red = parseTrustedEvidence(projection.evidence[`${prefix}red`]);
76
+ if (red === undefined || red.phase !== "red" || red.taskId !== projection.workItemId) {
77
+ throw new ControlPlaneStorageError("WSSPEC_EVENT_CHAIN_INVALID", "事件重放包含无效或不兼容的 Red Evidence。");
78
+ }
79
+ let cycle;
80
+ const greens = new Map();
81
+ for (const [key, value] of entries) {
82
+ if (key === `${prefix}red`)
83
+ continue;
84
+ if (key === `${prefix}cycle`) {
85
+ cycle = parseTddCycleEvidence(value);
86
+ if (cycle === undefined || cycle.taskId !== projection.workItemId || cycle.redEvidenceId !== red.evidenceId
87
+ || cycle.commandId !== red.commandId || cycle.testAssetsDigest !== red.testAssetsDigest
88
+ || !sameCanonicalValue(cycle.testPaths, red.testPaths)
89
+ || !sameCanonicalValue(cycle.testPathRules, red.testPathRules)
90
+ || !sameCanonicalValue(cycle.testAssets, red.testAssets)
91
+ || !sameCanonicalValue(cycle.testAssetPaths, red.testAssetPaths)
92
+ || !sameCanonicalValue(cycle.testAssetRoots, red.testAssetRoots)
93
+ || !sameCanonicalValue(cycle.productPaths, red.productPaths)) {
94
+ throw new ControlPlaneStorageError("WSSPEC_EVENT_CHAIN_INVALID", "事件重放包含无效或不兼容的 TDD Cycle Evidence。");
95
+ }
96
+ continue;
97
+ }
98
+ if (!key.startsWith(`${prefix}green:`))
99
+ throw new ControlPlaneStorageError("WSSPEC_EVENT_CHAIN_INVALID", `事件重放包含未知 TDD Evidence key:${key}`);
100
+ const green = parseTrustedEvidence(value);
101
+ if (green === undefined || green.phase !== "green" || green.taskId !== projection.workItemId
102
+ || key !== `${prefix}green:${green.evidenceId}`
103
+ || green.commandId !== red.commandId || green.commandDigest !== red.commandDigest
104
+ || green.testPathsDigest !== red.testPathsDigest || green.testAssetsDigest !== red.testAssetsDigest
105
+ || !sameCanonicalValue(green.testPaths, red.testPaths)
106
+ || !sameCanonicalValue(green.testFiles, red.testFiles)
107
+ || !sameCanonicalValue(green.testPathRules, red.testPathRules)
108
+ || !sameCanonicalValue(trustedTestAssetFiles(green.testAssets, green), trustedTestAssetFiles(red.testAssets, red))
109
+ || !sameCanonicalValue(green.testAssetPaths, red.testAssetPaths)
110
+ || !sameCanonicalValue(green.testAssetRoots, red.testAssetRoots)
111
+ || !sameCanonicalValue(green.productPaths, red.productPaths)) {
112
+ throw new ControlPlaneStorageError("WSSPEC_EVENT_CHAIN_INVALID", "事件重放包含未绑定当前 Red 的 Green Evidence。");
113
+ }
114
+ greens.set(green.evidenceId, green);
115
+ }
116
+ if (cycle !== undefined && (!greens.has(cycle.greenEvidenceId)
117
+ || (cycle.refactorEvidenceId !== undefined && !greens.has(cycle.refactorEvidenceId)))) {
118
+ throw new ControlPlaneStorageError("WSSPEC_EVENT_CHAIN_INVALID", "事件重放的 TDD Cycle 引用了不存在或不兼容的 Green Evidence。");
119
+ }
120
+ return { red, ...(cycle === undefined ? {} : { cycle }) };
121
+ }
122
+ async function assertRecoveredTddScope(worktree, projection) {
123
+ const { red } = assertedTddEvidence(projection);
124
+ if (red === undefined)
125
+ return;
126
+ let tests;
127
+ let assets;
128
+ try {
129
+ [tests, assets] = await Promise.all([
130
+ testFileManifest(worktree, red.testPaths, red.testPathRules),
131
+ testAssetScopeManifest(worktree, { testAssetPaths: red.testAssetPaths, testAssetRoots: red.testAssetRoots, productPaths: red.productPaths }),
132
+ ]);
133
+ }
134
+ catch {
135
+ throw new ControlPlaneStorageError("WSSPEC_EVENT_CHAIN_INVALID", "事件恢复时测试资产作用域已新增、删除或修改,旧 Red Evidence 失效。");
136
+ }
137
+ if (tests.digest !== red.testPathsDigest || assets.digest !== red.testAssetsDigest) {
138
+ throw new ControlPlaneStorageError("WSSPEC_EVENT_CHAIN_INVALID", `事件恢复时测试资产作用域摘要失配(tests ${red.testPathsDigest} -> ${tests.digest}; assets ${red.testAssetsDigest} -> ${assets.digest})。`);
139
+ }
140
+ }
141
+ async function readApplicationAnchorFile(controlPlane) {
142
+ try {
143
+ const anchor = JSON.parse(await readFile(path.join(controlPlane, "application-anchor.json"), "utf8"));
144
+ if (anchor.version !== 1 || typeof anchor.workItemId !== "string" || typeof anchor.manifestDigest !== "string" || typeof anchor.ownerToken !== "string") {
145
+ throw new ControlPlaneStorageError("WSSPEC_APPLICATION_ANCHOR_INVALID", "Application 锚点不完整。");
146
+ }
147
+ return anchor;
148
+ }
149
+ catch (error) {
150
+ if (error.code === "ENOENT")
151
+ return undefined;
152
+ throw error;
153
+ }
154
+ }
155
+ async function resolveControlPlane(cwd, workItemId) {
156
+ const repository = await loadRepository(cwd);
157
+ const locatorPath = path.join(repository.commonDir, "wsspec", "work-items", workItemId, "locator.json");
158
+ let locator;
159
+ try {
160
+ locator = JSON.parse(await readFile(locatorPath, "utf8"));
161
+ }
162
+ catch (error) {
163
+ if (error.code === "ENOENT") {
164
+ throw new ControlPlaneStorageError("WSSPEC_WORK_ITEM_NOT_FOUND", `找不到 Work Item:${workItemId}`);
165
+ }
166
+ throw error;
167
+ }
168
+ if (locator.repositoryId !== repository.repositoryId || locator.workItemId !== workItemId || typeof locator.worktree !== "string") {
169
+ throw new ControlPlaneStorageError("WSSPEC_REPOSITORY_ID_MISMATCH", "Work Item locator 与当前仓库身份不一致。");
170
+ }
171
+ const repositoryCache = JSON.parse(await readFile(path.join(repository.commonDir, "wsspec", "repository.json"), "utf8"));
172
+ if (repositoryCache.repositoryId !== repository.repositoryId || typeof repositoryCache.repositoryRoot !== "string") {
173
+ throw new ControlPlaneStorageError("WSSPEC_REPOSITORY_ID_MISMATCH", "Git common-dir 仓库缓存身份不一致。");
174
+ }
175
+ if (path.isAbsolute(locator.worktree) || locator.worktree.split(/[\\/]/u).includes("..")) {
176
+ throw new ControlPlaneStorageError("WSSPEC_WORK_ITEM_LOCATION_INVALID", "Work Item locator 必须指向仓库内相对 worktree。");
177
+ }
178
+ let cachedRoot;
179
+ let worktree;
180
+ try {
181
+ cachedRoot = await realpath(repositoryCache.repositoryRoot);
182
+ worktree = await realpath(path.resolve(cachedRoot, locator.worktree));
183
+ }
184
+ catch {
185
+ throw new ControlPlaneStorageError("WSSPEC_WORK_ITEM_LOCATION_INVALID", "Work Item locator 指向的 worktree 不存在。");
186
+ }
187
+ const worktreeRelative = path.relative(cachedRoot, worktree);
188
+ if (worktreeRelative === "" || worktreeRelative.startsWith("..") || path.isAbsolute(worktreeRelative)) {
189
+ throw new ControlPlaneStorageError("WSSPEC_WORK_ITEM_LOCATION_INVALID", "Work Item locator 的真实路径越出仓库边界。");
190
+ }
191
+ return {
192
+ directory: path.join(repository.commonDir, "wsspec", "work-items", workItemId, "control-plane"),
193
+ repositoryId: repository.repositoryId,
194
+ repositoryRoot: cachedRoot,
195
+ worktree: worktreeRelative,
196
+ };
197
+ }
198
+ function withoutLocation(projection) {
199
+ const { controlPlane: _controlPlane, ...stored } = projection;
200
+ return stored;
201
+ }
202
+ export async function writeProjection(projection) {
203
+ await writeFileAtomic(path.join(projection.controlPlane, "runtime.json"), `${JSON.stringify(withoutLocation(projection), null, 2)}\n`);
204
+ }
205
+ async function writeFileExclusive(target, content) {
206
+ const handle = await open(target, "wx", 0o600);
207
+ try {
208
+ await handle.writeFile(content, "utf8");
209
+ await handle.sync();
210
+ }
211
+ finally {
212
+ await handle.close();
213
+ }
214
+ }
215
+ export async function writeApplicationAnchor(input) {
216
+ const resolved = await resolveControlPlane(input.cwd, input.workItemId);
217
+ await mkdir(resolved.directory);
218
+ const anchor = { version: 1, workItemId: input.workItemId, manifestDigest: input.manifestDigest, ownerToken: input.ownerToken };
219
+ try {
220
+ await writeFileExclusive(path.join(resolved.directory, "application-anchor.json"), `${JSON.stringify(anchor, null, 2)}\n`);
221
+ }
222
+ catch (error) {
223
+ await rmdir(resolved.directory).catch((cleanupError) => {
224
+ if (!["ENOTEMPTY", "EEXIST"].includes(cleanupError.code ?? ""))
225
+ throw cleanupError;
226
+ });
227
+ throw error;
228
+ }
229
+ }
230
+ export async function readApplicationAnchor(cwd, workItemId) {
231
+ const resolved = await resolveControlPlane(cwd, workItemId);
232
+ const anchor = await readApplicationAnchorFile(resolved.directory);
233
+ if (anchor !== undefined && anchor.workItemId !== workItemId) {
234
+ throw new ControlPlaneStorageError("WSSPEC_REPOSITORY_ID_MISMATCH", "Application 锚点与 Work Item 身份不一致。");
235
+ }
236
+ return anchor;
237
+ }
238
+ export async function readCapturedSourceReference(cwd, workItemId) {
239
+ const resolved = await resolveControlPlane(cwd, workItemId);
240
+ return readCapturedSourceReferenceFromControlPlane(resolved.directory, workItemId);
241
+ }
242
+ export async function readCapturedSourceReferenceFromControlPlane(controlPlane, workItemId) {
243
+ const events = await readEvents(controlPlane).catch((error) => {
244
+ if (error instanceof EventStoreError)
245
+ throw new ControlPlaneStorageError(error.code, error.message);
246
+ throw error;
247
+ });
248
+ return sourceReferenceFromEvents(events, workItemId);
249
+ }
250
+ export async function authenticateApplicationSourceAuthority(input) {
251
+ const itemRoot = path.join(input.worktree, ".wsspec", "work-items", input.workItemId);
252
+ let applicationText;
253
+ try {
254
+ applicationText = await readFile(path.join(itemRoot, "snapshot", "application.json"), "utf8");
255
+ }
256
+ catch (error) {
257
+ if (error.code === "ENOENT") {
258
+ throw new ControlPlaneStorageError("WSSPEC_SOURCE_SNAPSHOT_CHANGED", "Work Item 缺少可信 Application 快照。");
259
+ }
260
+ throw error;
261
+ }
262
+ const manifestText = await readFile(path.join(itemRoot, "work-item.yaml"), "utf8");
263
+ const anchor = await readApplicationAnchorFile(input.controlPlane);
264
+ if (anchor === undefined) {
265
+ throw new ControlPlaneStorageError("WSSPEC_SOURCE_SNAPSHOT_CHANGED", "Work Item 缺少可信 Application 锚点。");
266
+ }
267
+ if (anchor.workItemId !== input.workItemId || sha256(manifestText) !== anchor.manifestDigest) {
268
+ throw new ControlPlaneStorageError("WSSPEC_WORK_ITEM_MANIFEST_CHANGED", "Work Item manifest 与可信 Application 锚点不一致。");
269
+ }
270
+ const manifest = parse(manifestText);
271
+ if (typeof manifest.execution?.workflowDigest !== "string" || sha256(applicationText) !== manifest.execution.workflowDigest) {
272
+ throw new ControlPlaneStorageError("WSSPEC_APPLICATION_SNAPSHOT_CHANGED", "Application 快照摘要与 Work Item manifest 不一致。");
273
+ }
274
+ let application;
275
+ try {
276
+ application = parseApplicationSnapshot(JSON.parse(applicationText));
277
+ }
278
+ catch {
279
+ throw new ControlPlaneStorageError("WSSPEC_APPLICATION_SNAPSHOT_INVALID", "Application 快照不是合法的严格快照。");
280
+ }
281
+ if (manifest.workItemId !== input.workItemId
282
+ || (input.repositoryId !== undefined && manifest.repositoryId !== input.repositoryId)) {
283
+ throw new ControlPlaneStorageError("WSSPEC_REPOSITORY_ID_MISMATCH", "Work Item manifest 与控制面身份不一致。");
284
+ }
285
+ const events = await readEvents(input.controlPlane).catch((error) => {
286
+ if (error instanceof EventStoreError)
287
+ throw new ControlPlaneStorageError(error.code, error.message);
288
+ throw error;
289
+ });
290
+ const eventSourceReference = sourceReferenceFromEvents(events, input.workItemId);
291
+ if (typeof manifest.source?.artifactId !== "string" || typeof manifest.source.snapshot !== "string"
292
+ || typeof manifest.source.artifactDigest !== "string" || typeof manifest.source.contentDigest !== "string"
293
+ || typeof manifest.source.type !== "string") {
294
+ throw new ControlPlaneStorageError("WSSPEC_SOURCE_SNAPSHOT_CHANGED", "Work Item 缺少严格 Source Artifact 引用。");
295
+ }
296
+ const manifestSourceReference = {
297
+ artifactType: "requirement-source",
298
+ schemaVersion: 1,
299
+ artifactId: manifest.source.artifactId,
300
+ path: `.wsspec/work-items/${input.workItemId}/${manifest.source.snapshot}`,
301
+ revision: 1,
302
+ contentHash: manifest.source.artifactDigest,
303
+ mediaType: "application/json",
304
+ };
305
+ if (!sameCanonicalValue(eventSourceReference, manifestSourceReference)
306
+ || !sameCanonicalValue(eventSourceReference, application.source)) {
307
+ throw new ControlPlaneStorageError("WSSPEC_SOURCE_SNAPSHOT_CHANGED", "Application、Work Item manifest 与可信 Source 捕获事件不一致。");
308
+ }
309
+ const source = await verifySourceArtifact(input.worktree, input.workItemId, eventSourceReference);
310
+ if (source.contentDigest !== manifest.source.contentDigest || source.type !== manifest.source.type) {
311
+ throw new ControlPlaneStorageError("WSSPEC_SOURCE_SNAPSHOT_CHANGED", "Source Artifact 与 Work Item manifest 不一致。");
312
+ }
313
+ return {
314
+ application,
315
+ applicationText,
316
+ events,
317
+ itemRoot,
318
+ manifest,
319
+ source,
320
+ sourceReference: eventSourceReference,
321
+ };
322
+ }
323
+ export async function writeArchiveSnapshot(input) {
324
+ assertExternalReceipts(input.projection.evidence, "WSSPEC_EVENT_CHAIN_INVALID");
325
+ const { controlPlane: _controlPlane, ...publicProjection } = input.projection;
326
+ const audit = {
327
+ version: 1,
328
+ repositoryId: input.projection.repositoryId,
329
+ workItemId: input.projection.workItemId,
330
+ closedAt: input.closedAt,
331
+ workspaceTreeDigest: input.workspaceTreeDigest,
332
+ artifactTreeDigest: input.artifactTreeDigest,
333
+ terminalEventHash: input.projection.lastEventHash,
334
+ projection: publicProjection,
335
+ };
336
+ await writeFileAtomic(path.join(input.worktree, ".wsspec", "archive", input.projection.workItemId, "audit.json"), `${JSON.stringify(audit, null, 2)}\n`);
337
+ }
338
+ export async function initializeControlPlane(input) {
339
+ const resolved = await resolveControlPlane(input.cwd, input.workItemId);
340
+ await mkdir(resolved.directory, { recursive: true });
341
+ const projection = {
342
+ version: 1,
343
+ repositoryId: resolved.repositoryId,
344
+ workItemId: input.workItemId,
345
+ workItem: input.initialWorkItem ?? { status: "draft" },
346
+ stages: input.initialStages ?? Object.fromEntries(input.stages.map((stage) => [stage, { status: "pending" }])),
347
+ lastSequence: 0,
348
+ lastEventHash: null,
349
+ idempotency: {},
350
+ profile: input.initialProfile ?? { mode: "quick", selected: "quick", provisional: false, reasonRuleIds: [], riskSignals: emptyRuntimeRiskSignals() },
351
+ claims: {},
352
+ contexts: {},
353
+ approvals: {},
354
+ externalActions: {},
355
+ externalActionIdempotency: {},
356
+ evidence: {},
357
+ loops: {},
358
+ retries: {},
359
+ readOnly: false,
360
+ controlPlane: resolved.directory,
361
+ };
362
+ await writeFileExclusive(path.join(projection.controlPlane, "runtime.json"), `${JSON.stringify(withoutLocation(projection), null, 2)}\n`);
363
+ return projection;
364
+ }
365
+ export async function readControlPlane(cwd, workItemId) {
366
+ const resolved = await resolveControlPlane(cwd, workItemId);
367
+ const stored = JSON.parse(await readFile(path.join(resolved.directory, "runtime.json"), "utf8"));
368
+ if (stored.repositoryId !== resolved.repositoryId || stored.workItemId !== workItemId) {
369
+ throw new ControlPlaneStorageError("WSSPEC_REPOSITORY_ID_MISMATCH", "运行投影与当前仓库身份不一致。");
370
+ }
371
+ assertExternalProjectionContract(stored);
372
+ const projection = {
373
+ ...stored,
374
+ profile: stored.profile === undefined
375
+ ? { mode: "quick", selected: "quick", provisional: false, reasonRuleIds: [], riskSignals: emptyRuntimeRiskSignals() }
376
+ : { ...stored.profile, riskSignals: stored.profile.riskSignals ?? emptyRuntimeRiskSignals() },
377
+ claims: stored.claims ?? {},
378
+ contexts: stored.contexts ?? {},
379
+ approvals: stored.approvals ?? {},
380
+ externalActions: stored.externalActions ?? {},
381
+ externalActionIdempotency: stored.externalActionIdempotency ?? {},
382
+ evidence: stored.evidence ?? {},
383
+ loops: stored.loops ?? {},
384
+ retries: stored.retries ?? {},
385
+ readOnly: stored.readOnly ?? false,
386
+ controlPlane: resolved.directory,
387
+ };
388
+ assertExternalActionProjection(projection.externalActions, projection.externalActionIdempotency);
389
+ return projection;
390
+ }
391
+ export function replayEvents(input) {
392
+ let recovered = {
393
+ version: 1,
394
+ repositoryId: input.repositoryId,
395
+ workItemId: input.workItemId,
396
+ workItem: input.initialWorkItem ?? { status: "draft" },
397
+ stages: input.initialStages ?? Object.fromEntries(input.stageIds.map((stage) => [stage, { status: "pending" }])),
398
+ lastSequence: 0,
399
+ lastEventHash: null,
400
+ idempotency: {},
401
+ profile: input.initialProfile ?? { mode: "quick", selected: "quick", provisional: false, reasonRuleIds: [], riskSignals: emptyRuntimeRiskSignals() },
402
+ claims: {},
403
+ contexts: {},
404
+ approvals: {},
405
+ externalActions: {},
406
+ externalActionIdempotency: {},
407
+ evidence: {},
408
+ loops: {},
409
+ retries: {},
410
+ readOnly: false,
411
+ controlPlane: input.controlPlane,
412
+ };
413
+ for (const event of input.events) {
414
+ if (event.repositoryId !== recovered.repositoryId || event.workItemId !== recovered.workItemId) {
415
+ throw new ControlPlaneStorageError("WSSPEC_REPOSITORY_ID_MISMATCH", "事件与当前仓库或 Work Item 身份不一致。");
416
+ }
417
+ if (event.eventType === "work-item.transitioned") {
418
+ recovered = { ...recovered, workItem: transitionWorkItem(recovered.workItem, { type: "transition", to: event.to }) };
419
+ }
420
+ else if (event.eventType === "stage.transitioned") {
421
+ const stageId = event.stageId;
422
+ if (stageId === null || recovered.stages[stageId] === undefined) {
423
+ throw new ControlPlaneStorageError("WSSPEC_EVENT_CHAIN_INVALID", "Stage 事件引用了未知 Stage。");
424
+ }
425
+ recovered = { ...recovered, stages: { ...recovered.stages, [stageId]: transitionStage(recovered.stages[stageId], { type: "transition", to: event.to }) } };
426
+ }
427
+ else if (event.eventType === "artifact.authored") {
428
+ const value = event.result.value;
429
+ if (value === null || typeof value !== "object" || Array.isArray(value)) {
430
+ throw new ControlPlaneStorageError("WSSPEC_EVENT_CHAIN_INVALID", "Artifact authoring 事件缺少安全摘要引用。");
431
+ }
432
+ }
433
+ else {
434
+ const result = event.result;
435
+ if (result.projection === undefined)
436
+ throw new ControlPlaneStorageError("WSSPEC_EVENT_CHAIN_INVALID", `事件 ${event.eventType} 缺少可重放投影。`);
437
+ recovered = { ...recovered, ...result.projection };
438
+ assertExternalActionProjection(recovered.externalActions, recovered.externalActionIdempotency);
439
+ assertExternalReceipts(recovered.evidence, "WSSPEC_EVENT_CHAIN_INVALID");
440
+ assertedTddEvidence(recovered);
441
+ }
442
+ recovered.lastSequence = event.sequence;
443
+ recovered.lastEventHash = event.eventHash;
444
+ recovered.idempotency[event.idempotencyKey] = event.sequence;
445
+ }
446
+ recovered.profile = {
447
+ ...recovered.profile,
448
+ riskSignals: recovered.profile.riskSignals ?? emptyRuntimeRiskSignals(),
449
+ };
450
+ recovered.readOnly = recovered.workItem.status === "closed";
451
+ return recovered;
452
+ }
453
+ export async function recoverControlPlane(input) {
454
+ const resolved = await resolveControlPlane(input.cwd, input.workItemId);
455
+ await recoverStaleControlPlaneLock(resolved.directory);
456
+ return withControlPlaneLock(resolved.directory, async () => {
457
+ await repairIncompleteEventTail(resolved.directory);
458
+ let durableProjection;
459
+ try {
460
+ const stored = JSON.parse(await readFile(path.join(resolved.directory, "runtime.json"), "utf8"));
461
+ if (stored.repositoryId !== resolved.repositoryId || stored.workItemId !== input.workItemId) {
462
+ throw new ControlPlaneStorageError("WSSPEC_REPOSITORY_ID_MISMATCH", "运行投影与当前仓库身份不一致。");
463
+ }
464
+ assertExternalProjectionContract(stored);
465
+ durableProjection = stored;
466
+ }
467
+ catch (error) {
468
+ if (error instanceof ControlPlaneStorageError)
469
+ throw error;
470
+ if (!(error instanceof SyntaxError) && error.code !== "ENOENT")
471
+ throw error;
472
+ }
473
+ const worktree = path.join(resolved.repositoryRoot, resolved.worktree);
474
+ const authority = await authenticateApplicationSourceAuthority({
475
+ controlPlane: resolved.directory,
476
+ worktree,
477
+ workItemId: input.workItemId,
478
+ repositoryId: resolved.repositoryId,
479
+ });
480
+ const events = authority.events;
481
+ let stageIds;
482
+ let initialWorkItem;
483
+ let initialStages;
484
+ let initialProfile;
485
+ const application = authority.application;
486
+ const profile = application.profiles[application.selectedProfile];
487
+ stageIds = profile.order;
488
+ initialWorkItem = { status: "active" };
489
+ initialStages = deriveInitialStages(profile);
490
+ initialProfile = { mode: application.selectedProfile, selected: application.selectedProfile, provisional: false, reasonRuleIds: [], riskSignals: emptyRuntimeRiskSignals() };
491
+ if (durableProjection !== undefined) {
492
+ const anchoredEvent = durableProjection.lastSequence === 0 ? undefined : events[durableProjection.lastSequence - 1];
493
+ if (durableProjection.lastSequence > events.length ||
494
+ (durableProjection.lastSequence > 0 && anchoredEvent?.eventHash !== durableProjection.lastEventHash)) {
495
+ throw new ControlPlaneStorageError("WSSPEC_EVENT_CHAIN_INVALID", "事件日志短于或偏离持久化投影锚点。");
496
+ }
497
+ }
498
+ let recovered = replayEvents({
499
+ repositoryId: resolved.repositoryId,
500
+ workItemId: input.workItemId,
501
+ stageIds,
502
+ controlPlane: resolved.directory,
503
+ events,
504
+ ...(initialWorkItem === undefined ? {} : { initialWorkItem }),
505
+ ...(initialStages === undefined ? {} : { initialStages }),
506
+ ...(initialProfile === undefined ? {} : { initialProfile }),
507
+ });
508
+ await assertRecoveredTddScope(path.join(resolved.repositoryRoot, resolved.worktree), recovered);
509
+ const recoveryTime = new Date();
510
+ const abandonedStages = Object.entries(recovered.stages).filter(([stageId, stage]) => {
511
+ if (stage.status === "running")
512
+ return true;
513
+ if (stage.status !== "claimed")
514
+ return false;
515
+ const expiresAt = Date.parse(recovered.claims[stageId]?.expiresAt ?? "");
516
+ return !Number.isFinite(expiresAt) || expiresAt <= recoveryTime.getTime();
517
+ }).map(([stageId]) => stageId);
518
+ const durableApprovalIds = new Set(Object.entries(recovered.approvals)
519
+ .filter(([, approval]) => approval.status === "pending"
520
+ && recovered.workItem.status === "awaiting_approval"
521
+ && recovered.stages[approval.stageId]?.status === "awaiting_approval")
522
+ .map(([requestId]) => requestId));
523
+ const durableApprovalStages = new Set([...durableApprovalIds].map((requestId) => recovered.approvals[requestId].stageId));
524
+ const invalidPendingApprovalIds = new Set(Object.entries(recovered.approvals)
525
+ .filter(([requestId, approval]) => approval.status === "pending" && !durableApprovalIds.has(requestId))
526
+ .map(([requestId]) => requestId));
527
+ const approvalStages = Object.entries(recovered.stages)
528
+ .filter(([stageId, stage]) => stage.status === "awaiting_approval" && !durableApprovalStages.has(stageId))
529
+ .map(([stageId]) => stageId);
530
+ const abandonedStageSet = new Set(abandonedStages);
531
+ const preDispatchExternalActionIds = Object.entries(recovered.externalActions)
532
+ .filter(([, action]) => abandonedStageSet.has(action.request.stepId)
533
+ && (action.status === "prepared" || action.status === "approved"
534
+ || (action.status === "executing" && action.dispatch === "not_sent")))
535
+ .map(([requestId]) => requestId);
536
+ const preservedExternalStageIds = [...new Set(Object.values(recovered.externalActions)
537
+ .filter((action) => abandonedStageSet.has(action.request.stepId)
538
+ && (action.status === "reconciliation_required" || action.status === "verified" || action.status === "failed"
539
+ || (action.status === "executing" && action.dispatch === "sent_or_unknown")))
540
+ .map((action) => action.request.stepId))];
541
+ const preservedExternalStageSet = new Set(preservedExternalStageIds);
542
+ const uncertainExternalActionIds = Object.entries(recovered.externalActions)
543
+ .filter(([, action]) => action.status === "executing" && action.dispatch === "sent_or_unknown")
544
+ .map(([requestId]) => requestId);
545
+ const recoveryStages = [...new Set([
546
+ ...abandonedStages.filter((stageId) => !preservedExternalStageSet.has(stageId)),
547
+ ...approvalStages,
548
+ ])];
549
+ const recoverWorkItem = recovered.workItem.status === "awaiting_approval" && durableApprovalIds.size === 0;
550
+ if (recoveryStages.length > 0 || recoverWorkItem || invalidPendingApprovalIds.size > 0
551
+ || uncertainExternalActionIds.length > 0 || preDispatchExternalActionIds.length > 0 || preservedExternalStageIds.length > 0) {
552
+ const recoveredAt = recoveryTime.toISOString();
553
+ const next = {
554
+ ...recovered,
555
+ workItem: recoverWorkItem ? { status: "active" } : recovered.workItem,
556
+ stages: { ...recovered.stages },
557
+ claims: { ...recovered.claims },
558
+ contexts: { ...recovered.contexts },
559
+ approvals: { ...recovered.approvals },
560
+ externalActions: { ...recovered.externalActions },
561
+ externalActionIdempotency: { ...recovered.externalActionIdempotency },
562
+ evidence: { ...recovered.evidence },
563
+ retries: { ...recovered.retries },
564
+ };
565
+ for (const requestId of preDispatchExternalActionIds) {
566
+ const action = next.externalActions[requestId];
567
+ if (action === undefined)
568
+ throw new ControlPlaneStorageError("WSSPEC_EVENT_CHAIN_INVALID", "待失效外部动作不存在。");
569
+ delete next.externalActions[requestId];
570
+ delete next.externalActionIdempotency[action.request.idempotencyKey];
571
+ delete next.evidence[`external-rejection:${requestId}`];
572
+ }
573
+ for (const stageId of recoveryStages) {
574
+ const stepInstanceId = recovered.claims[stageId]?.stageId ?? stageId;
575
+ const retry = Object.hasOwn(next.retries, stepInstanceId) ? next.retries[stepInstanceId] : undefined;
576
+ if (retry !== undefined)
577
+ next.retries[stepInstanceId] = interruptedRetry(retry);
578
+ next.stages[stageId] = (Object.hasOwn(next.retries, stepInstanceId) ? next.retries[stepInstanceId] : undefined)?.status === "exhausted"
579
+ ? { status: "failed" }
580
+ : { status: "ready" };
581
+ delete next.claims[stageId];
582
+ delete next.contexts[stageId];
583
+ }
584
+ for (const stageId of preservedExternalStageIds) {
585
+ const claim = next.claims[stageId];
586
+ const context = next.contexts[stageId];
587
+ if (claim === undefined || context?.workPackage?.attemptId !== claim.attemptId
588
+ || context.workPackage.lease?.token !== claim.claimToken) {
589
+ throw new ControlPlaneStorageError("WSSPEC_EVENT_CHAIN_INVALID", "外部动作恢复缺少原 Attempt/Lease 上下文。");
590
+ }
591
+ const claimedAt = Date.parse(claim.claimedAt);
592
+ const expiresAt = Date.parse(claim.expiresAt);
593
+ const leaseDuration = Number.isFinite(claimedAt) && Number.isFinite(expiresAt) && expiresAt > claimedAt
594
+ ? expiresAt - claimedAt
595
+ : 30 * 60 * 1_000;
596
+ const renewedExpiresAt = new Date(recoveryTime.getTime() + leaseDuration).toISOString();
597
+ const workPackage = {
598
+ ...context.workPackage,
599
+ lease: { ...context.workPackage.lease, expiresAt: renewedExpiresAt },
600
+ };
601
+ next.claims[stageId] = {
602
+ ...claim,
603
+ expiresAt: renewedExpiresAt,
604
+ workPackageDigest: workPackageIdentityDigest(workPackage),
605
+ };
606
+ next.contexts[stageId] = {
607
+ ...context,
608
+ workPackage,
609
+ };
610
+ }
611
+ for (const requestId of invalidPendingApprovalIds) {
612
+ const approval = next.approvals[requestId];
613
+ next.approvals[requestId] = { ...approval, status: "expired", decidedAt: recoveredAt };
614
+ }
615
+ for (const requestId of uncertainExternalActionIds) {
616
+ const action = next.externalActions[requestId];
617
+ if (action.status !== "executing")
618
+ throw new ControlPlaneStorageError("WSSPEC_EVENT_CHAIN_INVALID", "外部动作恢复状态不一致。");
619
+ next.externalActions[requestId] = {
620
+ status: "reconciliation_required",
621
+ request: action.request,
622
+ grant: action.grant,
623
+ reason: "process recovery after dispatch",
624
+ requiredAt: recoveredAt,
625
+ };
626
+ }
627
+ const previous = events.at(-1);
628
+ if (previous === undefined)
629
+ throw new ControlPlaneStorageError("WSSPEC_EVENT_CHAIN_INVALID", "活动 Claim 缺少对应事件历史。");
630
+ const idempotencyKey = `recover-interrupted:${previous.eventHash}`;
631
+ const event = {
632
+ eventId: `event-${crypto.randomUUID()}`, eventType: "projection.invalidated", occurredAt: new Date().toISOString(), actor: "recovery",
633
+ repositoryId: recovered.repositoryId, workItemId: recovered.workItemId, stageId: null, attemptId: null,
634
+ from: recovered.workItem.status, to: recovered.workItem.status, idempotencyKey,
635
+ workflowDigest: previous.workflowDigest, configDigest: previous.configDigest, baselineTreeDigest: previous.baselineTreeDigest,
636
+ inputWorkspaceTreeDigest: previous.outputWorkspaceTreeDigest ?? previous.inputWorkspaceTreeDigest, outputWorkspaceTreeDigest: null,
637
+ inputDigest: sha256(JSON.stringify({
638
+ recoveryStages: [...recoveryStages].sort(),
639
+ preDispatchExternalActionIds: [...preDispatchExternalActionIds].sort(),
640
+ preservedExternalStageIds: [...preservedExternalStageIds].sort(),
641
+ uncertainExternalActionIds: [...uncertainExternalActionIds].sort(),
642
+ })),
643
+ result: {
644
+ projection: {
645
+ workItem: next.workItem,
646
+ stages: next.stages,
647
+ profile: next.profile,
648
+ claims: next.claims,
649
+ contexts: next.contexts,
650
+ approvals: next.approvals,
651
+ externalActions: next.externalActions,
652
+ externalActionIdempotency: next.externalActionIdempotency,
653
+ evidence: next.evidence,
654
+ loops: next.loops,
655
+ retries: next.retries,
656
+ readOnly: next.readOnly,
657
+ },
658
+ value: {
659
+ abandonedStages,
660
+ approvalStages,
661
+ preDispatchExternalActionIds,
662
+ preservedExternalStageIds,
663
+ uncertainExternalActionIds,
664
+ },
665
+ },
666
+ };
667
+ const stored = await appendEventUnlocked(resolved.directory, event);
668
+ next.lastSequence = stored.sequence;
669
+ next.lastEventHash = stored.eventHash;
670
+ next.idempotency = { ...next.idempotency, [idempotencyKey]: stored.sequence };
671
+ recovered = next;
672
+ }
673
+ await writeProjection(recovered);
674
+ if (recovered.workItem.status === "closed") {
675
+ const closedEvent = [...events].reverse().find((event) => event.eventType === "work-item.closed");
676
+ const value = closedEvent?.result?.value;
677
+ const applicationClose = recovered.evidence[applicationCloseEvidenceKey];
678
+ const closedAt = value?.closedAt ?? applicationClose?.closedAt;
679
+ const workspaceTreeDigest = value?.workspaceTreeDigest ?? applicationClose?.workspaceTreeDigest;
680
+ const artifactTreeDigest = applicationClose?.artifactTreeDigest;
681
+ if (typeof closedAt !== "string" || typeof workspaceTreeDigest !== "string" || typeof artifactTreeDigest !== "string")
682
+ throw new ControlPlaneStorageError("WSSPEC_EVENT_CHAIN_INVALID", "关闭事件缺少归档重建数据。");
683
+ await writeArchiveSnapshot({ projection: recovered, worktree: path.join(resolved.repositoryRoot, resolved.worktree), closedAt, workspaceTreeDigest, artifactTreeDigest });
684
+ }
685
+ return recovered;
686
+ });
687
+ }
688
+ //# sourceMappingURL=control-plane.js.map