ai-delivery-workflow 0.2.2

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 (211) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +112 -0
  3. package/bin/ai-delivery.mjs +138 -0
  4. package/bin/validate-skills.mjs +12 -0
  5. package/docs/DUAL-REPOSITORY-WORKSPACE.zh-CN.md +125 -0
  6. package/docs/EVOLUTION-MAINTENANCE.zh-CN.md +75 -0
  7. package/docs/EVOLUTION-USER-GUIDE.zh-CN.md +160 -0
  8. package/docs/FILE-REFERENCE.zh-CN.md +503 -0
  9. package/docs/PROJECT-MANUAL.zh-CN.md +931 -0
  10. package/docs/STATE-CLI-MAINTENANCE.zh-CN.md +135 -0
  11. package/docs/STATE-CLI-USER-GUIDE.zh-CN.md +186 -0
  12. package/docs/VIEWER-MAINTENANCE.zh-CN.md +373 -0
  13. package/docs/VIEWER-USER-GUIDE.zh-CN.md +235 -0
  14. package/lib/delivery-state.mjs +1220 -0
  15. package/lib/evolution.mjs +971 -0
  16. package/lib/project-bootstrap.mjs +927 -0
  17. package/lib/project-installer.mjs +1184 -0
  18. package/lib/skill-validator.mjs +154 -0
  19. package/lib/toml-hooks.mjs +97 -0
  20. package/lib/workspace.mjs +302 -0
  21. package/lib/yaml-runtime.mjs +18 -0
  22. package/package.json +39 -0
  23. package/skills/ai-delivery-assemble-release/SKILL.md +27 -0
  24. package/skills/ai-delivery-assemble-release/agents/openai.yaml +4 -0
  25. package/skills/ai-delivery-assemble-release/assets/release-candidate-manifest.yaml +22 -0
  26. package/skills/ai-delivery-assemble-release/assets/release-git-plan.yaml +9 -0
  27. package/skills/ai-delivery-assemble-release/assets/release-material-index.csv +2 -0
  28. package/skills/ai-delivery-assemble-release/assets/version-inclusion.csv +2 -0
  29. package/skills/ai-delivery-assemble-release/references/release-assembly-contract.md +7 -0
  30. package/skills/ai-delivery-assemble-release/scripts/plan-release.mjs +298 -0
  31. package/skills/ai-delivery-bootstrap/SKILL.md +59 -0
  32. package/skills/ai-delivery-bootstrap/agents/openai.yaml +4 -0
  33. package/skills/ai-delivery-bootstrap/references/bootstrap-contract.md +70 -0
  34. package/skills/ai-delivery-checkpoint-task/SKILL.md +63 -0
  35. package/skills/ai-delivery-checkpoint-task/agents/openai.yaml +4 -0
  36. package/skills/ai-delivery-checkpoint-task/assets/codex-hook-config.toml +92 -0
  37. package/skills/ai-delivery-checkpoint-task/assets/runtime-template/resume.md +15 -0
  38. package/skills/ai-delivery-checkpoint-task/assets/runtime-template/task.json +30 -0
  39. package/skills/ai-delivery-checkpoint-task/assets/runtime-template/version-archive.json +10 -0
  40. package/skills/ai-delivery-checkpoint-task/references/checkpoint-contract.md +95 -0
  41. package/skills/ai-delivery-checkpoint-task/references/checkpoint-recovery.zh-CN.md +111 -0
  42. package/skills/ai-delivery-checkpoint-task/scripts/hook-event.mjs +131 -0
  43. package/skills/ai-delivery-checkpoint-task/scripts/task-state.mjs +1073 -0
  44. package/skills/ai-delivery-close-version/SKILL.md +33 -0
  45. package/skills/ai-delivery-close-version/agents/openai.yaml +4 -0
  46. package/skills/ai-delivery-close-version/assets/line-advance-record.yaml +12 -0
  47. package/skills/ai-delivery-close-version/assets/version-lineage.yaml +12 -0
  48. package/skills/ai-delivery-close-version/assets/version-manifest.yaml +20 -0
  49. package/skills/ai-delivery-close-version/references/version-closeout-contract.md +20 -0
  50. package/skills/ai-delivery-define-product/SKILL.md +76 -0
  51. package/skills/ai-delivery-define-product/agents/openai.yaml +4 -0
  52. package/skills/ai-delivery-define-product/assets/product-template/acceptance-criteria.csv +1 -0
  53. package/skills/ai-delivery-define-product/assets/product-template/discovery-baseline.md +19 -0
  54. package/skills/ai-delivery-define-product/assets/product-template/feature-detail.md +19 -0
  55. package/skills/ai-delivery-define-product/assets/product-template/feature-terminals.csv +1 -0
  56. package/skills/ai-delivery-define-product/assets/product-template/features.csv +1 -0
  57. package/skills/ai-delivery-define-product/assets/product-template/product-baseline.yaml +8 -0
  58. package/skills/ai-delivery-define-product/assets/product-template/product-manual.md +24 -0
  59. package/skills/ai-delivery-define-product/assets/product-template/terminals.csv +1 -0
  60. package/skills/ai-delivery-define-product/assets/product-template/user-stories.csv +1 -0
  61. package/skills/ai-delivery-define-product/references/product-contract.md +57 -0
  62. package/skills/ai-delivery-deploy-production/SKILL.md +45 -0
  63. package/skills/ai-delivery-deploy-production/agents/openai.yaml +4 -0
  64. package/skills/ai-delivery-deploy-production/references/deployment-contract.md +7 -0
  65. package/skills/ai-delivery-design-architecture/SKILL.md +67 -0
  66. package/skills/ai-delivery-design-architecture/agents/openai.yaml +4 -0
  67. package/skills/ai-delivery-design-architecture/assets/architecture-template/architecture-baseline.md +25 -0
  68. package/skills/ai-delivery-design-architecture/assets/architecture-template/architecture-gate.yaml +15 -0
  69. package/skills/ai-delivery-design-architecture/assets/architecture-template/prototype-architecture-validation.yaml +58 -0
  70. package/skills/ai-delivery-design-architecture/references/architecture-contract.md +32 -0
  71. package/skills/ai-delivery-design-experience/SKILL.md +71 -0
  72. package/skills/ai-delivery-design-experience/agents/openai.yaml +4 -0
  73. package/skills/ai-delivery-design-experience/assets/experience-template/canvas-catalog.csv +1 -0
  74. package/skills/ai-delivery-design-experience/assets/experience-template/canvas-pages.csv +1 -0
  75. package/skills/ai-delivery-design-experience/assets/experience-template/component-state-matrix.csv +1 -0
  76. package/skills/ai-delivery-design-experience/assets/experience-template/design-tokens.json +19 -0
  77. package/skills/ai-delivery-design-experience/assets/experience-template/experience-baseline.yaml +17 -0
  78. package/skills/ai-delivery-design-experience/assets/experience-template/experience-change-domains.csv +1 -0
  79. package/skills/ai-delivery-design-experience/assets/experience-template/experience-change-set.csv +1 -0
  80. package/skills/ai-delivery-design-experience/assets/experience-template/feature-screen-coverage.csv +1 -0
  81. package/skills/ai-delivery-design-experience/assets/experience-template/formal-ui-confirmation.yaml +33 -0
  82. package/skills/ai-delivery-design-experience/assets/experience-template/interaction-contract.csv +1 -0
  83. package/skills/ai-delivery-design-experience/assets/experience-template/low-fidelity-confirmation.yaml +32 -0
  84. package/skills/ai-delivery-design-experience/assets/experience-template/page-catalog.csv +1 -0
  85. package/skills/ai-delivery-design-experience/assets/experience-template/page-component-map.csv +1 -0
  86. package/skills/ai-delivery-design-experience/assets/experience-template/product-prototype-reconciliation.yaml +28 -0
  87. package/skills/ai-delivery-design-experience/assets/experience-template/prototype-adjustment-log.csv +1 -0
  88. package/skills/ai-delivery-design-experience/assets/experience-template/prototype-confirmation.yaml +99 -0
  89. package/skills/ai-delivery-design-experience/assets/experience-template/prototype-file-terminals.csv +1 -0
  90. package/skills/ai-delivery-design-experience/assets/experience-template/prototype-files.csv +1 -0
  91. package/skills/ai-delivery-design-experience/assets/experience-template/prototype-impact-assessment.yaml +25 -0
  92. package/skills/ai-delivery-design-experience/assets/experience-template/prototype-manifest.yaml +74 -0
  93. package/skills/ai-delivery-design-experience/assets/experience-template/prototype-product-coverage.csv +1 -0
  94. package/skills/ai-delivery-design-experience/assets/experience-template/prototype-review-comments.csv +1 -0
  95. package/skills/ai-delivery-design-experience/assets/experience-template/prototype-review-decision.yaml +29 -0
  96. package/skills/ai-delivery-design-experience/assets/experience-template/prototype-review-sessions.csv +1 -0
  97. package/skills/ai-delivery-design-experience/assets/experience-template/prototype-set.yaml +22 -0
  98. package/skills/ai-delivery-design-experience/assets/experience-template/prototype-tool-candidates.csv +1 -0
  99. package/skills/ai-delivery-design-experience/assets/experience-template/prototype-tool-selection.yaml +22 -0
  100. package/skills/ai-delivery-design-experience/assets/experience-template/prototype-traceability.csv +1 -0
  101. package/skills/ai-delivery-design-experience/assets/experience-template/requirement-change-set.csv +1 -0
  102. package/skills/ai-delivery-design-experience/assets/experience-template/screen-states.csv +1 -0
  103. package/skills/ai-delivery-design-experience/assets/experience-template/ui-framework-candidates.csv +1 -0
  104. package/skills/ai-delivery-design-experience/assets/experience-template/ui-framework-selection.yaml +18 -0
  105. package/skills/ai-delivery-design-experience/assets/experience-template/ui-handoff.yaml +44 -0
  106. package/skills/ai-delivery-design-experience/assets/experience-template/visual-direction.yaml +16 -0
  107. package/skills/ai-delivery-design-experience/assets/experience-template/visual-system-confirmation.yaml +31 -0
  108. package/skills/ai-delivery-design-experience/references/experience-contract.md +82 -0
  109. package/skills/ai-delivery-design-experience/references/prototype-management-contract.md +68 -0
  110. package/skills/ai-delivery-design-tests/SKILL.md +56 -0
  111. package/skills/ai-delivery-design-tests/agents/openai.yaml +4 -0
  112. package/skills/ai-delivery-design-tests/assets/test-design-template/iteration-test-contract.md +19 -0
  113. package/skills/ai-delivery-design-tests/assets/test-design-template/prototype-test-scope.csv +1 -0
  114. package/skills/ai-delivery-design-tests/assets/test-design-template/prototype-test-validation.yaml +73 -0
  115. package/skills/ai-delivery-design-tests/references/test-design-contract.md +31 -0
  116. package/skills/ai-delivery-develop-iteration/SKILL.md +66 -0
  117. package/skills/ai-delivery-develop-iteration/agents/openai.yaml +4 -0
  118. package/skills/ai-delivery-develop-iteration/assets/development-template/task-experience-change-traceability.csv +1 -0
  119. package/skills/ai-delivery-develop-iteration/assets/development-template/task-prototype-traceability.csv +1 -0
  120. package/skills/ai-delivery-develop-iteration/references/development-contract.md +39 -0
  121. package/skills/ai-delivery-evolve-workflow/SKILL.md +57 -0
  122. package/skills/ai-delivery-evolve-workflow/agents/openai.yaml +4 -0
  123. package/skills/ai-delivery-evolve-workflow/references/evolution-contract.md +42 -0
  124. package/skills/ai-delivery-execute-test/SKILL.md +34 -0
  125. package/skills/ai-delivery-execute-test/agents/openai.yaml +4 -0
  126. package/skills/ai-delivery-execute-test/references/test-execution-contract.md +7 -0
  127. package/skills/ai-delivery-execute-work-package/SKILL.md +53 -0
  128. package/skills/ai-delivery-execute-work-package/agents/openai.yaml +4 -0
  129. package/skills/ai-delivery-execute-work-package/references/work-package-contract.md +20 -0
  130. package/skills/ai-delivery-manage-git/SKILL.md +60 -0
  131. package/skills/ai-delivery-manage-git/agents/openai.yaml +4 -0
  132. package/skills/ai-delivery-manage-git/references/git-policy.md +42 -0
  133. package/skills/ai-delivery-manage-standards/SKILL.md +44 -0
  134. package/skills/ai-delivery-manage-standards/agents/openai.yaml +4 -0
  135. package/skills/ai-delivery-manage-standards/assets/standards-template/standard.md +25 -0
  136. package/skills/ai-delivery-manage-standards/references/standards-contract.md +20 -0
  137. package/skills/ai-delivery-orchestrate/SKILL.md +105 -0
  138. package/skills/ai-delivery-orchestrate/agents/openai.yaml +4 -0
  139. package/skills/ai-delivery-orchestrate/assets/project-template/.codex/config.toml +92 -0
  140. package/skills/ai-delivery-orchestrate/assets/project-template/.gitattributes +6 -0
  141. package/skills/ai-delivery-orchestrate/assets/project-template/.workflow/delivery/artifact-registry.yaml +3 -0
  142. package/skills/ai-delivery-orchestrate/assets/project-template/.workflow/delivery/project.yaml +6 -0
  143. package/skills/ai-delivery-orchestrate/assets/project-template/.workflow/delivery/standards-baseline.yaml +8 -0
  144. package/skills/ai-delivery-orchestrate/assets/project-template/.workflow/delivery/workflow-state.yaml +34 -0
  145. package/skills/ai-delivery-orchestrate/assets/project-template/.workflow/evolution/decisions/.gitkeep +0 -0
  146. package/skills/ai-delivery-orchestrate/assets/project-template/.workflow/evolution/experiments/.gitkeep +0 -0
  147. package/skills/ai-delivery-orchestrate/assets/project-template/.workflow/evolution/feedback/.gitkeep +0 -0
  148. package/skills/ai-delivery-orchestrate/assets/project-template/.workflow/evolution/index.yaml +7 -0
  149. package/skills/ai-delivery-orchestrate/assets/project-template/.workflow/evolution/promotion/.gitkeep +0 -0
  150. package/skills/ai-delivery-orchestrate/assets/project-template/.workflow/evolution/proposals/.gitkeep +0 -0
  151. package/skills/ai-delivery-orchestrate/assets/project-template/.workflow/extensions/.gitkeep +0 -0
  152. package/skills/ai-delivery-orchestrate/assets/project-template/.workflow/standards/STD-ARTIFACT-PATH-001.md +47 -0
  153. package/skills/ai-delivery-orchestrate/assets/project-template/.workflow/standards/STD-DOC-LANG-001.md +62 -0
  154. package/skills/ai-delivery-orchestrate/assets/project-template/.workflow/standards/STD-TEST-LOCATION-001.md +23 -0
  155. package/skills/ai-delivery-orchestrate/assets/project-template/.workflow/standards/index.yaml +36 -0
  156. package/skills/ai-delivery-orchestrate/assets/project-template/.workflow/tools/bootstrap/bootstrap.mjs +33 -0
  157. package/skills/ai-delivery-orchestrate/assets/project-template/.workflow/tools/evolution/evolve.mjs +15 -0
  158. package/skills/ai-delivery-orchestrate/assets/project-template/.workflow/tools/hooks/hook-event.mjs +131 -0
  159. package/skills/ai-delivery-orchestrate/assets/project-template/.workflow/tools/state/state.mjs +21 -0
  160. package/skills/ai-delivery-orchestrate/assets/project-template/.workflow/tools/viewer/public/app.js +546 -0
  161. package/skills/ai-delivery-orchestrate/assets/project-template/.workflow/tools/viewer/public/index.html +199 -0
  162. package/skills/ai-delivery-orchestrate/assets/project-template/.workflow/tools/viewer/public/styles.css +529 -0
  163. package/skills/ai-delivery-orchestrate/assets/project-template/.workflow/tools/viewer/server.mjs +780 -0
  164. package/skills/ai-delivery-orchestrate/assets/project-template/.workflow/tools/viewer/start.cmd +4 -0
  165. package/skills/ai-delivery-orchestrate/assets/project-template/.workflow/tools/viewer/start.ps1 +2 -0
  166. package/skills/ai-delivery-orchestrate/assets/project-template/.workflow/tools/viewer/start.sh +4 -0
  167. package/skills/ai-delivery-orchestrate/assets/project-template/AGENTS.md +44 -0
  168. package/skills/ai-delivery-orchestrate/references/formal-state-contract.md +64 -0
  169. package/skills/ai-delivery-orchestrate/references/workflow-model.md +29 -0
  170. package/skills/ai-delivery-orchestrate-release/SKILL.md +33 -0
  171. package/skills/ai-delivery-orchestrate-release/agents/openai.yaml +4 -0
  172. package/skills/ai-delivery-orchestrate-release/assets/release-request.yaml +9 -0
  173. package/skills/ai-delivery-orchestrate-release/assets/version-selection.yaml +16 -0
  174. package/skills/ai-delivery-orchestrate-release/references/release-workflow-contract.md +11 -0
  175. package/skills/ai-delivery-plan-iteration/SKILL.md +57 -0
  176. package/skills/ai-delivery-plan-iteration/agents/openai.yaml +4 -0
  177. package/skills/ai-delivery-plan-iteration/assets/planning-template/backlog.csv +1 -0
  178. package/skills/ai-delivery-plan-iteration/assets/planning-template/slice-dependencies.csv +1 -0
  179. package/skills/ai-delivery-plan-iteration/assets/planning-template/slice-experience-change-traceability.csv +1 -0
  180. package/skills/ai-delivery-plan-iteration/assets/planning-template/slice-prototype-traceability.csv +1 -0
  181. package/skills/ai-delivery-plan-iteration/assets/planning-template/slice-sources.csv +1 -0
  182. package/skills/ai-delivery-plan-iteration/assets/planning-template/vertical-slices.csv +1 -0
  183. package/skills/ai-delivery-plan-iteration/references/planning-contract.md +40 -0
  184. package/skills/ai-delivery-prepare-platform/SKILL.md +49 -0
  185. package/skills/ai-delivery-prepare-platform/agents/openai.yaml +4 -0
  186. package/skills/ai-delivery-prepare-platform/assets/platform-template/environment-ledger.csv +1 -0
  187. package/skills/ai-delivery-prepare-platform/references/platform-contract.md +18 -0
  188. package/skills/ai-delivery-prepare-release/SKILL.md +47 -0
  189. package/skills/ai-delivery-prepare-release/agents/openai.yaml +4 -0
  190. package/skills/ai-delivery-prepare-release/assets/release-template/delivery-items.csv +1 -0
  191. package/skills/ai-delivery-prepare-release/assets/release-template/release-manifest.yaml +18 -0
  192. package/skills/ai-delivery-prepare-release/references/release-contract.md +21 -0
  193. package/skills/ai-delivery-review-change/SKILL.md +40 -0
  194. package/skills/ai-delivery-review-change/agents/openai.yaml +4 -0
  195. package/skills/ai-delivery-review-change/references/review-contract.md +20 -0
  196. package/skills/ai-delivery-triage-failure/SKILL.md +45 -0
  197. package/skills/ai-delivery-triage-failure/agents/openai.yaml +4 -0
  198. package/skills/ai-delivery-triage-failure/references/failure-routing-contract.md +13 -0
  199. package/skills/ai-delivery-validate-artifacts/SKILL.md +37 -0
  200. package/skills/ai-delivery-validate-artifacts/agents/openai.yaml +4 -0
  201. package/skills/ai-delivery-validate-artifacts/references/artifact-contract.md +44 -0
  202. package/skills/ai-delivery-verify-candidate/SKILL.md +56 -0
  203. package/skills/ai-delivery-verify-candidate/agents/openai.yaml +4 -0
  204. package/skills/ai-delivery-verify-candidate/assets/verification-template/defect-artifacts.csv +1 -0
  205. package/skills/ai-delivery-verify-candidate/assets/verification-template/defects.csv +1 -0
  206. package/skills/ai-delivery-verify-candidate/assets/verification-template/test-work-packages.csv +1 -0
  207. package/skills/ai-delivery-verify-candidate/references/candidate-verification-contract.md +23 -0
  208. package/skills/ai-delivery-verify-production/SKILL.md +50 -0
  209. package/skills/ai-delivery-verify-production/agents/openai.yaml +4 -0
  210. package/skills/ai-delivery-verify-production/assets/production-verification-template/production-verification.md +15 -0
  211. package/skills/ai-delivery-verify-production/references/production-verification-contract.md +14 -0
@@ -0,0 +1,1220 @@
1
+ import crypto from "node:crypto";
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+
5
+ const NODE_IDS = new Set([
6
+ "00-bootstrap",
7
+ "01-product-discovery",
8
+ "02-product-definition",
9
+ "03-architecture-initial",
10
+ "04-experience-design",
11
+ "05-architecture-reconcile",
12
+ "06-platform-preparation",
13
+ "07-iteration-planning",
14
+ "08-test-design",
15
+ "09-development",
16
+ "10-change-review",
17
+ "11-candidate-verification",
18
+ "12-version-closeout",
19
+ "12-release-preparation",
20
+ "13-production-approval",
21
+ "14-production-deployment",
22
+ "15-production-verification",
23
+ "16-version-archive",
24
+ "R00-release-request",
25
+ "R01-version-selection",
26
+ "R02-release-eligibility",
27
+ "R03-material-aggregation",
28
+ "R04-release-candidate",
29
+ "R05-release-verification",
30
+ "R06-release-preparation",
31
+ "R07-production-approval",
32
+ "R08-production-deployment",
33
+ "R09-production-verification",
34
+ "R10-release-archive",
35
+ ]);
36
+ const ARTIFACT_STATUSES = new Set([
37
+ "draft",
38
+ "review-draft",
39
+ "pending-approval",
40
+ "approved",
41
+ "valid",
42
+ "invalid",
43
+ "blocked",
44
+ "stale",
45
+ "superseded",
46
+ "frozen",
47
+ "immutable",
48
+ "passed",
49
+ "failed",
50
+ "deployed",
51
+ "released",
52
+ "release-ready",
53
+ "included-in-release",
54
+ "rolled-back",
55
+ "archived",
56
+ ]);
57
+ const GATE_DECISIONS = new Set(["approved", "rejected", "blocked", "not-required"]);
58
+ const UX_GATE_IDS = new Set(["UX-LF", "UX-VS", "UX-UI"]);
59
+ const SUCCESSFUL_ARTIFACT_STATUSES = new Set([
60
+ "approved", "valid", "frozen", "immutable", "passed", "deployed", "released",
61
+ "release-ready", "included-in-release", "archived",
62
+ ]);
63
+ const NODE_RULES = new Map([
64
+ ["00-bootstrap", { scope: "development", prerequisites: [], gates: [] }],
65
+ ["01-product-discovery", { scope: "development", prerequisites: ["00-bootstrap"], gates: ["GATE-A"] }],
66
+ ["02-product-definition", { scope: "development", prerequisites: ["01-product-discovery"], gates: ["GATE-B"] }],
67
+ ["03-architecture-initial", { scope: "development", prerequisites: ["02-product-definition"], gates: [] }],
68
+ ["04-experience-design", { scope: "development", prerequisites: ["03-architecture-initial"], gates: ["UX-LF", "UX-VS", "UX-UI"] }],
69
+ ["05-architecture-reconcile", { scope: "development", prerequisites: ["04-experience-design"], gates: [] }],
70
+ ["06-platform-preparation", { scope: "development", prerequisites: ["03-architecture-initial"], gates: [] }],
71
+ ["07-iteration-planning", { scope: "development", prerequisites: ["05-architecture-reconcile", "06-platform-preparation"], gates: [] }],
72
+ ["08-test-design", { scope: "development", prerequisites: ["07-iteration-planning"], gates: [] }],
73
+ ["09-development", { scope: "development", prerequisites: ["08-test-design"], gates: [] }],
74
+ ["10-change-review", { scope: "development", prerequisites: ["09-development"], gates: [] }],
75
+ ["11-candidate-verification", { scope: "development", prerequisites: ["10-change-review"], gates: [] }],
76
+ ["12-version-closeout", { scope: "development", prerequisites: ["11-candidate-verification"], gates: [] }],
77
+ ["R00-release-request", { scope: "release", prerequisites: [], gates: [] }],
78
+ ["R01-version-selection", { scope: "release", prerequisites: ["R00-release-request"], gates: [] }],
79
+ ["R02-release-eligibility", { scope: "release", prerequisites: ["R01-version-selection"], gates: [] }],
80
+ ["R03-material-aggregation", { scope: "release", prerequisites: ["R02-release-eligibility"], gates: [] }],
81
+ ["R04-release-candidate", { scope: "release", prerequisites: ["R03-material-aggregation"], gates: [] }],
82
+ ["R05-release-verification", { scope: "release", prerequisites: ["R04-release-candidate"], gates: [] }],
83
+ ["R06-release-preparation", { scope: "release", prerequisites: ["R05-release-verification"], gates: [] }],
84
+ ["R07-production-approval", { scope: "release", prerequisites: ["R06-release-preparation"], gates: ["PRODUCTION-APPROVAL"] }],
85
+ ["R08-production-deployment", { scope: "release", prerequisites: ["R07-production-approval"], gates: [] }],
86
+ ["R09-production-verification", { scope: "release", prerequisites: ["R08-production-deployment"], gates: [] }],
87
+ ["R10-release-archive", { scope: "release", prerequisites: ["R09-production-verification"], gates: [] }],
88
+ ]);
89
+ const WRITABLE_NODE_IDS = new Set(NODE_RULES.keys());
90
+
91
+ function parseArgs(argv) {
92
+ const positional = [];
93
+ const options = new Map();
94
+ for (let index = 0; index < argv.length; index += 1) {
95
+ const value = argv[index];
96
+ if (!value.startsWith("--")) {
97
+ positional.push(value);
98
+ continue;
99
+ }
100
+ const next = argv[index + 1];
101
+ if (!next || next.startsWith("--")) {
102
+ options.set(value, true);
103
+ continue;
104
+ }
105
+ const current = options.get(value);
106
+ options.set(value, current === undefined ? next : Array.isArray(current) ? [...current, next] : [current, next]);
107
+ index += 1;
108
+ }
109
+ return { positional, options };
110
+ }
111
+
112
+ function lifecycleDescriptor(command, positional, flags) {
113
+ if (command === "artifact" && new Set(["register", "status"]).has(positional[1])) {
114
+ return {
115
+ kind: "artifact",
116
+ action: `artifact-${positional[1]}`,
117
+ subject_id: optionalFlag(flags, "--id") || optionalFlag(flags, "--artifact-id"),
118
+ };
119
+ }
120
+ if (command === "gate" && new Set(["request", "decide"]).has(positional[1])) {
121
+ return { kind: "gate", action: `gate-${positional[1]}`, subject_id: optionalFlag(flags, "--gate-id") };
122
+ }
123
+ if (command !== "transition") return null;
124
+ const transition = optionalFlag(flags, "--event") || "transition";
125
+ if (transition.startsWith("node-")) {
126
+ return { kind: "node", action: transition, subject_id: optionalFlag(flags, "--node") };
127
+ }
128
+ if (transition.startsWith("release-")) {
129
+ return { kind: "release", action: transition, subject_id: optionalFlag(flags, "--release-id") };
130
+ }
131
+ return {
132
+ kind: "version",
133
+ action: transition,
134
+ subject_id: optionalFlag(flags, "--iteration-id") || optionalFlag(flags, "--version-id"),
135
+ };
136
+ }
137
+
138
+ function dispatchLifecycle(options, files, descriptor, phase, flags, result = null, error = null) {
139
+ if (!descriptor || typeof options.onLifecycleEvent !== "function") return { decision: "pass" };
140
+ const revision = optionalFlag(flags, "--expected-revision") || "none";
141
+ const identity = descriptor.subject_id || "unspecified";
142
+ const idempotencyKey = `formal-state-${descriptor.kind}-${phase}-${descriptor.action}-${identity}-r${revision}`;
143
+ const event = {
144
+ schema_version: 1,
145
+ event_id: `EVT-${sha256(idempotencyKey).slice(0, 24)}`,
146
+ idempotency_key: idempotencyKey,
147
+ event_type: `${descriptor.kind}.${phase}`,
148
+ occurred_at: (options.now || (() => new Date().toISOString()))(),
149
+ sequence: revision,
150
+ scope: {
151
+ project_root: files.root,
152
+ subject_id: descriptor.subject_id,
153
+ action: descriptor.action,
154
+ },
155
+ source_ref: { command: descriptor.action, expected_revision: revision },
156
+ payload: error
157
+ ? { outcome: "failed", error: error.message }
158
+ : { outcome: phase === "before" ? "pending" : "succeeded", revision: result?.revision ?? null },
159
+ };
160
+ try {
161
+ return options.onLifecycleEvent(event) || { decision: "pass" };
162
+ } catch (hookError) {
163
+ return { decision: "warn", messages: [`Evolution Hook dispatch failed: ${hookError.message}`], evidence_refs: [] };
164
+ }
165
+ }
166
+
167
+ function requireYaml(yaml) {
168
+ if (typeof yaml?.parse !== "function" || typeof yaml?.stringify !== "function") {
169
+ throw new Error("A YAML parser and stringifier are required.");
170
+ }
171
+ }
172
+
173
+ function readYaml(file, yaml, label) {
174
+ if (!fs.existsSync(file)) throw new Error(`${label} is missing: ${file}`);
175
+ const value = yaml.parse(fs.readFileSync(file, "utf8"));
176
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
177
+ throw new Error(`${label} must contain a YAML mapping.`);
178
+ }
179
+ return value;
180
+ }
181
+
182
+ function atomicWrite(file, content) {
183
+ fs.mkdirSync(path.dirname(file), { recursive: true });
184
+ const temporary = `${file}.${process.pid}.${Date.now()}.tmp`;
185
+ fs.writeFileSync(temporary, content, "utf8");
186
+ fs.renameSync(temporary, file);
187
+ }
188
+
189
+ function isInside(candidate, root) {
190
+ const relative = path.relative(path.resolve(root), path.resolve(candidate));
191
+ return relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative));
192
+ }
193
+
194
+ function sha256(value) {
195
+ return crypto.createHash("sha256").update(value).digest("hex");
196
+ }
197
+
198
+ function requiredFlag(flags, name) {
199
+ const raw = flags.get(name);
200
+ const value = Array.isArray(raw) ? raw.at(-1) : raw;
201
+ if (value === undefined || value === true || String(value).trim() === "") {
202
+ throw new Error(`Missing required option: ${name}`);
203
+ }
204
+ return String(value).trim();
205
+ }
206
+
207
+ function optionalFlag(flags, name) {
208
+ const raw = flags.get(name);
209
+ const value = Array.isArray(raw) ? raw.at(-1) : raw;
210
+ return value === undefined || value === true ? null : String(value).trim();
211
+ }
212
+
213
+ function repeatedFlag(flags, name) {
214
+ const raw = flags.get(name);
215
+ if (raw === undefined || raw === true) return [];
216
+ return (Array.isArray(raw) ? raw : [raw]).map((value) => String(value).trim()).filter(Boolean);
217
+ }
218
+
219
+ function artifactStatus(flags) {
220
+ const status = requiredFlag(flags, "--status").toLowerCase().replaceAll("_", "-");
221
+ if (!ARTIFACT_STATUSES.has(status)) throw new Error(`Unsupported artifact status: ${status}`);
222
+ return status;
223
+ }
224
+
225
+ function expectedRevision(flags) {
226
+ const raw = requiredFlag(flags, "--expected-revision");
227
+ const value = Number(raw);
228
+ if (!Number.isInteger(value) || value < 0) {
229
+ throw new Error("--expected-revision must be a non-negative integer.");
230
+ }
231
+ return value;
232
+ }
233
+
234
+ function withLock(files, stream, action) {
235
+ fs.mkdirSync(files.lockRoot, { recursive: true });
236
+ const lockFile = path.join(files.lockRoot, "formal-state.lock");
237
+ let descriptor;
238
+ try {
239
+ descriptor = fs.openSync(lockFile, "wx");
240
+ fs.writeFileSync(descriptor, `${JSON.stringify({
241
+ pid: process.pid,
242
+ created_at: new Date().toISOString(),
243
+ })}\n`);
244
+ } catch (error) {
245
+ if (error.code === "EEXIST") throw new Error(`Formal state is locked while updating: ${stream}`);
246
+ throw error;
247
+ }
248
+ try {
249
+ return action();
250
+ } finally {
251
+ if (descriptor !== undefined) fs.closeSync(descriptor);
252
+ fs.rmSync(lockFile, { force: true });
253
+ }
254
+ }
255
+
256
+ function statePaths(projectRoot) {
257
+ const root = path.resolve(projectRoot);
258
+ const workflow = path.join(root, ".workflow");
259
+ const delivery = path.join(root, ".workflow", "delivery");
260
+ return {
261
+ root,
262
+ workflow,
263
+ delivery,
264
+ project: path.join(delivery, "project.yaml"),
265
+ workflowState: path.join(delivery, "workflow-state.yaml"),
266
+ artifactRegistry: path.join(delivery, "artifact-registry.yaml"),
267
+ events: path.join(delivery, "state-events.jsonl"),
268
+ eventShards: path.join(workflow, "control", "events"),
269
+ lockRoot: path.join(delivery, "runtime", "formal-state-locks"),
270
+ };
271
+ }
272
+
273
+ function normalizeScope(value, scopeId = null) {
274
+ return {
275
+ scope_id: value?.scope_id ?? scopeId,
276
+ revision: value?.revision ?? 0,
277
+ active_nodes: Array.isArray(value?.active_nodes) ? value.active_nodes : [],
278
+ blocked_nodes: Array.isArray(value?.blocked_nodes) ? value.blocked_nodes : [],
279
+ completed_nodes: Array.isArray(value?.completed_nodes) ? value.completed_nodes : [],
280
+ pending_gates: Array.isArray(value?.pending_gates) ? value.pending_gates : [],
281
+ gate_decisions: Array.isArray(value?.gate_decisions) ? value.gate_decisions : [],
282
+ node_attempts: value?.node_attempts && typeof value.node_attempts === "object"
283
+ ? value.node_attempts
284
+ : {},
285
+ latest_candidate_id: value?.latest_candidate_id ?? null,
286
+ latest_release_id: value?.latest_release_id ?? null,
287
+ updated_at: value?.updated_at ?? "",
288
+ last_transition: value?.last_transition ?? null,
289
+ };
290
+ }
291
+
292
+ function exposeLegacyScopeAliases(state) {
293
+ const preferred = state.last_transition?.scope === "release"
294
+ ? state.release_state
295
+ : state.development_state.scope_id ? state.development_state : state.release_state;
296
+ const aliases = {
297
+ iteration_id: preferred.scope_id,
298
+ active_nodes: preferred.active_nodes,
299
+ blocked_nodes: preferred.blocked_nodes,
300
+ completed_nodes: preferred.completed_nodes,
301
+ pending_gates: preferred.pending_gates,
302
+ gate_decisions: preferred.gate_decisions,
303
+ latest_candidate_id: state.development_state.latest_candidate_id
304
+ || state.release_state.latest_candidate_id,
305
+ latest_release_id: state.release_state.latest_release_id,
306
+ };
307
+ for (const [key, alias] of Object.entries(aliases)) {
308
+ Object.defineProperty(state, key, { value: alias, enumerable: false, configurable: true });
309
+ }
310
+ return state;
311
+ }
312
+
313
+ function normalizeWorkflowState(value) {
314
+ const legacyNodes = [
315
+ ...(Array.isArray(value.active_nodes) ? value.active_nodes : []),
316
+ ...(Array.isArray(value.blocked_nodes) ? value.blocked_nodes : []),
317
+ ...(Array.isArray(value.completed_nodes) ? value.completed_nodes : []),
318
+ ];
319
+ const legacyRelease = legacyNodes.some((node) => String(node).startsWith("R"))
320
+ || String(value.iteration_id || "").startsWith("REL-");
321
+ const legacyScope = normalizeScope({
322
+ scope_id: value.iteration_id ?? null,
323
+ revision: value.revision ?? 0,
324
+ active_nodes: value.active_nodes,
325
+ blocked_nodes: value.blocked_nodes,
326
+ completed_nodes: value.completed_nodes,
327
+ pending_gates: value.pending_gates,
328
+ gate_decisions: value.gate_decisions,
329
+ latest_candidate_id: value.latest_candidate_id,
330
+ latest_release_id: value.latest_release_id,
331
+ updated_at: value.updated_at,
332
+ last_transition: value.last_transition,
333
+ });
334
+ const state = {
335
+ schema_version: value.schema_version ?? 1,
336
+ revision: value.revision ?? 0,
337
+ project_id: value.project_id ?? null,
338
+ development_state: normalizeScope(
339
+ value.development_state || (!legacyRelease ? legacyScope : null),
340
+ !legacyRelease ? value.iteration_id ?? null : null,
341
+ ),
342
+ release_state: normalizeScope(
343
+ value.release_state || (legacyRelease ? legacyScope : null),
344
+ legacyRelease ? value.iteration_id ?? null : null,
345
+ ),
346
+ latest_line_version_id: value.latest_line_version_id ?? null,
347
+ production_version_id: value.production_version_id ?? null,
348
+ production_release_id: value.production_release_id ?? null,
349
+ updated_at: value.updated_at ?? "",
350
+ last_transition: value.last_transition ?? null,
351
+ };
352
+ return exposeLegacyScopeAliases(state);
353
+ }
354
+
355
+ function scopeNameForNode(node) {
356
+ return NODE_RULES.get(node)?.scope || (String(node).startsWith("R") ? "release" : "development");
357
+ }
358
+
359
+ function scopeState(state, scopeName) {
360
+ return scopeName === "release" ? state.release_state : state.development_state;
361
+ }
362
+
363
+ function normalizeArtifactRegistry(value) {
364
+ return {
365
+ ...value,
366
+ schema_version: value.schema_version ?? 1,
367
+ revision: value.revision ?? 0,
368
+ artifacts: Array.isArray(value.artifacts) ? value.artifacts : [],
369
+ };
370
+ }
371
+
372
+ function appendEvent(files, event) {
373
+ const existing = fs.existsSync(files.events) ? fs.readFileSync(files.events, "utf8") : "";
374
+ const prefix = existing && !existing.endsWith("\n") ? `${existing}\n` : existing;
375
+ atomicWrite(files.events, `${prefix}${JSON.stringify(event)}\n`);
376
+ }
377
+
378
+ function writeStream(files, stream, file, value, event, options) {
379
+ const serialized = options.yaml.stringify(value);
380
+ const eventId = crypto.randomUUID();
381
+ const eventRecord = {
382
+ event_id: eventId,
383
+ stream,
384
+ revision: value.revision,
385
+ occurred_at: event.occurred_at,
386
+ ...event,
387
+ after_checksum: sha256(serialized),
388
+ };
389
+ const shardFile = path.join(files.eventShards, stream, `${String(value.revision).padStart(6, "0")}-${eventId}.yaml`);
390
+ atomicWrite(shardFile, options.yaml.stringify({ ...eventRecord, after_value: value }));
391
+ atomicWrite(file, serialized);
392
+ appendEvent(files, eventRecord);
393
+ }
394
+
395
+ function readEventShards(files, options) {
396
+ if (!fs.existsSync(files.eventShards)) return [];
397
+ const records = [];
398
+ for (const stream of fs.readdirSync(files.eventShards).sort()) {
399
+ const directory = path.join(files.eventShards, stream);
400
+ if (!fs.statSync(directory).isDirectory()) continue;
401
+ for (const name of fs.readdirSync(directory).filter((item) => /\.ya?ml$/i.test(item)).sort()) {
402
+ const record = readYaml(path.join(directory, name), options.yaml, `formal event shard ${stream}/${name}`);
403
+ if (record.stream !== stream || !Number.isInteger(record.revision) || !record.after_value) {
404
+ throw new Error(`Formal event shard is invalid: ${stream}/${name}`);
405
+ }
406
+ const checksum = sha256(options.yaml.stringify(record.after_value));
407
+ if (checksum !== record.after_checksum) throw new Error(`Formal event shard checksum mismatch: ${stream}/${name}`);
408
+ records.push(record);
409
+ }
410
+ }
411
+ return records.sort((left, right) =>
412
+ String(left.occurred_at).localeCompare(String(right.occurred_at))
413
+ || left.stream.localeCompare(right.stream)
414
+ || left.revision - right.revision);
415
+ }
416
+
417
+ function rebuildFormalViews(files, options) {
418
+ return withLock(files, "formal-state-rebuild", () => {
419
+ const records = readEventShards(files, options);
420
+ if (!records.length) {
421
+ return { command: "rebuild", rebuilt: false, event_shard_count: 0, message: "No formal event shards exist; legacy views were left unchanged." };
422
+ }
423
+ const latest = new Map();
424
+ for (const record of records) {
425
+ const current = latest.get(record.stream);
426
+ if (!current || record.revision > current.revision) latest.set(record.stream, record);
427
+ }
428
+ const workflow = latest.get("workflow-state");
429
+ const registry = latest.get("artifact-registry");
430
+ if (workflow) atomicWrite(files.workflowState, options.yaml.stringify(workflow.after_value));
431
+ if (registry) atomicWrite(files.artifactRegistry, options.yaml.stringify(registry.after_value));
432
+ const index = records.map(({ after_value: ignored, ...record }) => JSON.stringify(record)).join("\n");
433
+ atomicWrite(files.events, `${index}\n`);
434
+ return {
435
+ command: "rebuild",
436
+ rebuilt: true,
437
+ event_shard_count: records.length,
438
+ workflow_revision: workflow?.revision ?? 0,
439
+ artifact_registry_revision: registry?.revision ?? 0,
440
+ };
441
+ });
442
+ }
443
+
444
+ function registerArtifact(files, flags, options) {
445
+ const expected = expectedRevision(flags);
446
+ return withLock(files, "artifact-registry", () => {
447
+ const project = readYaml(files.project, options.yaml, "project.yaml");
448
+ const workflowState = normalizeWorkflowState(
449
+ readYaml(files.workflowState, options.yaml, "workflow-state.yaml"),
450
+ );
451
+ const registry = normalizeArtifactRegistry(
452
+ readYaml(files.artifactRegistry, options.yaml, "artifact-registry.yaml"),
453
+ );
454
+ if (registry.revision !== expected) {
455
+ throw new Error(`Artifact registry revision conflict: expected ${expected}, actual ${registry.revision}.`);
456
+ }
457
+
458
+ const artifactId = requiredFlag(flags, "--id");
459
+ if (registry.artifacts.some((item) => (item.artifact_id || item.id) === artifactId)) {
460
+ throw new Error(`Artifact ID is already registered: ${artifactId}`);
461
+ }
462
+ const artifactPath = requiredFlag(flags, "--path").replaceAll("\\", "/");
463
+ if (!artifactPath.startsWith(".workflow/")) {
464
+ throw new Error("Artifact path must be project-relative and start with .workflow/.");
465
+ }
466
+ if (registry.artifacts.some((item) =>
467
+ typeof item.path === "string" && item.path.replaceAll("\\", "/") === artifactPath)) {
468
+ throw new Error(`Artifact path is already registered: ${artifactPath}`);
469
+ }
470
+ const absolute = path.resolve(files.root, artifactPath);
471
+ if (!isInside(absolute, files.workflow) || !fs.existsSync(absolute) || !fs.statSync(absolute).isFile()) {
472
+ throw new Error(`Artifact file does not exist inside .workflow/: ${artifactPath}`);
473
+ }
474
+ if (!isInside(fs.realpathSync(absolute), fs.realpathSync(files.workflow))) {
475
+ throw new Error(`Artifact path resolves outside .workflow/: ${artifactPath}`);
476
+ }
477
+
478
+ const timestamp = (options.now || (() => new Date().toISOString()))();
479
+ const content = fs.readFileSync(absolute);
480
+ const supersedes = optionalFlag(flags, "--supersedes");
481
+ let predecessor = null;
482
+ if (supersedes) {
483
+ predecessor = registry.artifacts.find((item) => (item.artifact_id || item.id) === supersedes);
484
+ if (!predecessor) throw new Error(`Superseded artifact is not registered: ${supersedes}`);
485
+ if (predecessor.status === "superseded") {
486
+ throw new Error(`Artifact is already superseded: ${supersedes}`);
487
+ }
488
+ }
489
+ const node = requiredFlag(flags, "--node");
490
+ if (!NODE_IDS.has(node)) throw new Error(`Unknown workflow node: ${node}`);
491
+ const artifactScope = scopeNameForNode(node);
492
+ const artifactScopeState = scopeState(workflowState, artifactScope);
493
+ const artifact = {
494
+ artifact_id: artifactId,
495
+ artifact_type: optionalFlag(flags, "--artifact-type"),
496
+ path: artifactPath,
497
+ node,
498
+ scope: artifactScope,
499
+ iteration_id: artifactScopeState.scope_id || null,
500
+ status: artifactStatus(flags),
501
+ version: optionalFlag(flags, "--version"),
502
+ producer_skill: optionalFlag(flags, "--producer-skill"),
503
+ supersedes,
504
+ checksum_algorithm: "sha256",
505
+ checksum: sha256(content),
506
+ registered_at: timestamp,
507
+ };
508
+ if (predecessor) {
509
+ const previous = predecessor.status || "unknown";
510
+ predecessor.status = "superseded";
511
+ predecessor.superseded_by = artifactId;
512
+ predecessor.status_history = [
513
+ ...(Array.isArray(predecessor.status_history) ? predecessor.status_history : []),
514
+ {
515
+ from: previous,
516
+ to: "superseded",
517
+ evidence: [artifactId],
518
+ changed_at: timestamp,
519
+ },
520
+ ];
521
+ }
522
+ registry.schema_version = 1;
523
+ registry.project_id = registry.project_id || project.project_id || workflowState.project_id || null;
524
+ registry.iteration_id = workflowState.development_state.scope_id || registry.iteration_id || null;
525
+ registry.revision += 1;
526
+ registry.updated_at = timestamp;
527
+ registry.artifacts = [...registry.artifacts, artifact];
528
+ writeStream(files, "artifact-registry", files.artifactRegistry, registry, {
529
+ type: "artifact-registered",
530
+ occurred_at: timestamp,
531
+ artifact_id: artifactId,
532
+ path: artifactPath,
533
+ }, options);
534
+ return {
535
+ command: "artifact register",
536
+ project_id: registry.project_id,
537
+ iteration_id: registry.iteration_id,
538
+ revision: registry.revision,
539
+ artifact,
540
+ artifact_registry: registry,
541
+ };
542
+ });
543
+ }
544
+
545
+ function changeArtifactStatus(files, flags, options) {
546
+ const expected = expectedRevision(flags);
547
+ return withLock(files, "artifact-registry", () => {
548
+ const registry = normalizeArtifactRegistry(
549
+ readYaml(files.artifactRegistry, options.yaml, "artifact-registry.yaml"),
550
+ );
551
+ if (registry.revision !== expected) {
552
+ throw new Error(`Artifact registry revision conflict: expected ${expected}, actual ${registry.revision}.`);
553
+ }
554
+ const artifactId = requiredFlag(flags, "--id");
555
+ const artifact = registry.artifacts.find((item) => (item.artifact_id || item.id) === artifactId);
556
+ if (!artifact) throw new Error(`Artifact is not registered: ${artifactId}`);
557
+ const status = artifactStatus(flags);
558
+ const evidence = repeatedFlag(flags, "--evidence");
559
+ if (!evidence.length) throw new Error("artifact status requires at least one --evidence reference.");
560
+ if (artifact.status === status) throw new Error(`Artifact already has status ${status}: ${artifactId}`);
561
+ if (artifact.status === "superseded" || artifact.status === "archived") {
562
+ throw new Error(`Terminal artifact status cannot change: ${artifact.status}`);
563
+ }
564
+
565
+ const timestamp = (options.now || (() => new Date().toISOString()))();
566
+ const previous = artifact.status || "unknown";
567
+ artifact.status = status;
568
+ artifact.status_history = [
569
+ ...(Array.isArray(artifact.status_history) ? artifact.status_history : []),
570
+ { from: previous, to: status, evidence, changed_at: timestamp },
571
+ ];
572
+ registry.schema_version = 1;
573
+ registry.revision += 1;
574
+ registry.updated_at = timestamp;
575
+ writeStream(files, "artifact-registry", files.artifactRegistry, registry, {
576
+ type: "artifact-status-changed",
577
+ occurred_at: timestamp,
578
+ artifact_id: artifactId,
579
+ from: previous,
580
+ to: status,
581
+ evidence,
582
+ }, options);
583
+ return {
584
+ command: "artifact status",
585
+ revision: registry.revision,
586
+ artifact,
587
+ artifact_registry: registry,
588
+ };
589
+ });
590
+ }
591
+
592
+ function allowedCompletionStatuses(node) {
593
+ if (node === "12-version-closeout") return new Set(["release-ready"]);
594
+ if (node === "R08-production-deployment") return new Set(["deployed"]);
595
+ if (node === "R09-production-verification") return new Set(["passed"]);
596
+ if (node === "R10-release-archive") return new Set(["released", "archived"]);
597
+ return SUCCESSFUL_ARTIFACT_STATUSES;
598
+ }
599
+
600
+ function satisfiesMandatoryGate(node, decision) {
601
+ return decision?.decision === "approved"
602
+ || (node === "04-experience-design" && decision?.decision === "not-required");
603
+ }
604
+
605
+ function isProductTerminalEvidence(artifact) {
606
+ return artifact?.node === "02-product-definition"
607
+ && /terminal/i.test(artifact.artifact_type || "")
608
+ && SUCCESSFUL_ARTIFACT_STATUSES.has(artifact.status);
609
+ }
610
+
611
+ function persistScopeMutation(files, state, scope, timestamp, transition, eventRecord, options) {
612
+ state.schema_version = 2;
613
+ state.revision += 1;
614
+ scope.revision += 1;
615
+ scope.updated_at = timestamp;
616
+ state.updated_at = timestamp;
617
+ scope.last_transition = transition;
618
+ state.last_transition = transition;
619
+ writeStream(files, "workflow-state", files.workflowState, state, eventRecord, options);
620
+ exposeLegacyScopeAliases(state);
621
+ }
622
+
623
+ function transitionWorkflow(files, flags, options) {
624
+ const expected = expectedRevision(flags);
625
+ return withLock(files, "workflow-state", () => {
626
+ const state = normalizeWorkflowState(readYaml(files.workflowState, options.yaml, "workflow-state.yaml"));
627
+ if (state.revision !== expected) {
628
+ throw new Error(`Workflow state revision conflict: expected ${expected}, actual ${state.revision}.`);
629
+ }
630
+ const event = requiredFlag(flags, "--event");
631
+ const iterationId = requiredFlag(flags, "--iteration-id");
632
+ const evidence = repeatedFlag(flags, "--evidence");
633
+ const timestamp = (options.now || (() => new Date().toISOString()))();
634
+
635
+ const persist = (scopeName, details = {}) => {
636
+ const scope = scopeState(state, scopeName);
637
+ const transition = {
638
+ event,
639
+ scope: scopeName,
640
+ iteration_id: iterationId,
641
+ evidence,
642
+ ...details,
643
+ occurred_at: timestamp,
644
+ };
645
+ persistScopeMutation(files, state, scope, timestamp, transition, {
646
+ type: event,
647
+ occurred_at: timestamp,
648
+ scope: scopeName,
649
+ iteration_id: iterationId,
650
+ evidence,
651
+ ...details,
652
+ }, options);
653
+ return { command: "transition", event, revision: state.revision, scope_revision: scope.revision, workflow_state: state };
654
+ };
655
+
656
+ if (event === "iteration-started" || event === "release-started") {
657
+ const scopeName = event === "release-started" ? "release" : "development";
658
+ const current = scopeState(state, scopeName);
659
+ if (current.scope_id === iterationId) throw new Error(`${scopeName} scope is already current: ${iterationId}`);
660
+ if (current.active_nodes.length || current.blocked_nodes.length || current.pending_gates.length) {
661
+ throw new Error(`${event} requires its ${scopeName} scope to have no active nodes, blocked nodes, or pending gates.`);
662
+ }
663
+ if (event === "iteration-started") registeredEvidence(files, evidence, options);
664
+ const nextRevision = current.revision;
665
+ state[scopeName === "release" ? "release_state" : "development_state"] = normalizeScope({
666
+ scope_id: iterationId,
667
+ revision: nextRevision,
668
+ });
669
+ const project = readYaml(files.project, options.yaml, "project.yaml");
670
+ if (scopeName === "development") project.current_iteration_id = iterationId;
671
+ else project.current_release_id = iterationId;
672
+ atomicWrite(files.project, options.yaml.stringify(project));
673
+ return persist(scopeName);
674
+ }
675
+
676
+ if (event === "candidate-selected" || event === "release-selected") {
677
+ const artifactId = requiredFlag(flags, "--artifact-id");
678
+ const registry = normalizeArtifactRegistry(readYaml(files.artifactRegistry, options.yaml, "artifact-registry.yaml"));
679
+ const artifact = registry.artifacts.find((item) => (item.artifact_id || item.id) === artifactId);
680
+ if (!artifact) throw new Error(`Selected artifact is not registered: ${artifactId}`);
681
+ const expectedNodes = event === "candidate-selected"
682
+ ? new Set(["09-development", "R04-release-candidate"])
683
+ : new Set(["12-release-preparation", "R06-release-preparation"]);
684
+ if (!expectedNodes.has(artifact.node)) {
685
+ throw new Error(`Selected artifact ${artifactId} belongs to ${artifact.node}, not ${[...expectedNodes].join(" or ")}.`);
686
+ }
687
+ const scopeName = event === "release-selected" ? "release" : scopeNameForNode(artifact.node);
688
+ const scope = scopeState(state, scopeName);
689
+ if (!scope.scope_id) throw new Error(`${scopeName} scope must be started before ${event}.`);
690
+ if (scope.scope_id !== iterationId) {
691
+ throw new Error(`Iteration mismatch for ${scopeName} scope: state is ${scope.scope_id}, command is ${iterationId}.`);
692
+ }
693
+ if (artifact.iteration_id && artifact.iteration_id !== iterationId
694
+ && !(event === "release-selected" && artifact.node === "12-release-preparation")) {
695
+ throw new Error(`Selected artifact ${artifactId} belongs to iteration ${artifact.iteration_id}, not ${iterationId}.`);
696
+ }
697
+ const allowed = event === "candidate-selected" ? new Set(["immutable"]) : new Set(["immutable", "approved"]);
698
+ if (!allowed.has(artifact.status)) throw new Error(`Selected artifact ${artifactId} is not immutable or approved.`);
699
+ if (event === "release-selected") scope.latest_release_id = artifactId;
700
+ else scope.latest_candidate_id = artifactId;
701
+ return persist(scopeName, { artifact_id: artifactId, evidence: [artifactId] });
702
+ }
703
+
704
+ if (event === "version-closed" || event === "release-archived") {
705
+ const scopeName = event === "release-archived" ? "release" : "development";
706
+ const scope = scopeState(state, scopeName);
707
+ if (scope.scope_id !== iterationId) {
708
+ throw new Error(`Iteration mismatch for ${scopeName} scope: state is ${scope.scope_id}, command is ${iterationId}.`);
709
+ }
710
+ const artifactId = requiredFlag(flags, "--artifact-id");
711
+ const registry = normalizeArtifactRegistry(readYaml(files.artifactRegistry, options.yaml, "artifact-registry.yaml"));
712
+ const artifact = registry.artifacts.find((item) => (item.artifact_id || item.id) === artifactId);
713
+ if (!artifact) throw new Error(`Milestone artifact is not registered: ${artifactId}`);
714
+ const expectedNode = event === "version-closed" ? "12-version-closeout" : "R10-release-archive";
715
+ const expectedStatus = event === "version-closed" ? "release-ready" : "released";
716
+ if (!scope.completed_nodes.includes(expectedNode)) throw new Error(`${event} requires ${expectedNode} to be completed.`);
717
+ if (artifact.node !== expectedNode) throw new Error(`Milestone artifact ${artifactId} belongs to ${artifact.node}, not ${expectedNode}.`);
718
+ if (artifact.status !== expectedStatus) {
719
+ throw new Error(`Milestone artifact ${artifactId} must have status ${expectedStatus}, not ${artifact.status || "missing"}.`);
720
+ }
721
+ if (artifact.iteration_id && artifact.iteration_id !== iterationId) {
722
+ throw new Error(`Milestone artifact ${artifactId} belongs to iteration ${artifact.iteration_id}, not ${iterationId}.`);
723
+ }
724
+ if (event === "version-closed") {
725
+ if (state.latest_line_version_id === iterationId) throw new Error(`Version is already closed on the cumulative line: ${iterationId}`);
726
+ state.latest_line_version_id = iterationId;
727
+ } else {
728
+ if (state.production_release_id === iterationId) throw new Error(`Release is already archived as production: ${iterationId}`);
729
+ state.production_version_id = requiredFlag(flags, "--production-version-id");
730
+ state.production_release_id = iterationId;
731
+ }
732
+ return persist(scopeName, {
733
+ artifact_id: artifactId,
734
+ evidence: [artifactId],
735
+ ...(event === "release-archived" ? { production_version_id: state.production_version_id } : {}),
736
+ });
737
+ }
738
+
739
+ const node = requiredFlag(flags, "--node");
740
+ if (!WRITABLE_NODE_IDS.has(node)) {
741
+ if (NODE_IDS.has(node)) throw new Error(`Legacy workflow node is read-only: ${node}`);
742
+ throw new Error(`Unknown workflow node: ${node}`);
743
+ }
744
+ const scopeName = scopeNameForNode(node);
745
+ const scope = scopeState(state, scopeName);
746
+ if (!scope.scope_id) throw new Error(`${scopeName} scope must be started before node ${node}.`);
747
+ if (scope.scope_id !== iterationId) {
748
+ throw new Error(`Iteration mismatch for ${scopeName} scope: state is ${scope.scope_id}, command is ${iterationId}.`);
749
+ }
750
+ let reason = null;
751
+
752
+ if (event === "node-started") {
753
+ if (scope.blocked_nodes.includes(node)) throw new Error(`Blocked node must be unblocked before start: ${node}`);
754
+ if (scope.active_nodes.includes(node)) throw new Error(`Node is already active: ${node}`);
755
+ if (scope.completed_nodes.includes(node)) {
756
+ reason = requiredFlag(flags, "--reason");
757
+ scope.completed_nodes = scope.completed_nodes.filter((item) => item !== node);
758
+ }
759
+ const evidenceArtifacts = evidence.length ? registeredEvidence(files, evidence, options) : [];
760
+ if (node === "R00-release-request") {
761
+ const requestArtifact = evidenceArtifacts.find((artifact) =>
762
+ artifact.node === node
763
+ && artifact.artifact_type === "release-request"
764
+ && SUCCESSFUL_ARTIFACT_STATUSES.has(artifact.status));
765
+ if (!requestArtifact) {
766
+ throw new Error("R00-release-request requires successful explicit user request evidence with artifact type release-request.");
767
+ }
768
+ }
769
+ const prerequisites = NODE_RULES.get(node)?.prerequisites || [];
770
+ const missing = prerequisites.filter((prerequisite) =>
771
+ !scope.completed_nodes.includes(prerequisite)
772
+ && !evidenceArtifacts.some((artifact) => artifact.node === prerequisite
773
+ && SUCCESSFUL_ARTIFACT_STATUSES.has(artifact.status)));
774
+ if (missing.length) throw new Error(`Node prerequisite is not satisfied: ${missing.join(", ")}.`);
775
+ scope.active_nodes = [...scope.active_nodes, node];
776
+ scope.node_attempts[node] = Number(scope.node_attempts[node] || 0) + 1;
777
+ } else if (event === "node-completed") {
778
+ if (!scope.active_nodes.includes(node)) throw new Error(`Node is not active: ${node}`);
779
+ if (!evidence.length) throw new Error("node-completed requires at least one --evidence artifact ID.");
780
+ const artifacts = registeredEvidence(files, evidence, options, node);
781
+ const allowed = allowedCompletionStatuses(node);
782
+ const unsuccessful = artifacts.find((artifact) => !allowed.has(artifact.status));
783
+ if (unsuccessful) {
784
+ throw new Error(`Completion evidence ${unsuccessful.artifact_id || unsuccessful.id} has unsuccessful status ${unsuccessful.status}.`);
785
+ }
786
+ const attempt = Number(scope.node_attempts[node] || 1);
787
+ const pendingForNode = scope.pending_gates.filter((gate) =>
788
+ typeof gate === "string" || (gate?.node === node && Number(gate.node_attempt || attempt) === attempt));
789
+ if (pendingForNode.length) throw new Error(`Node has pending gates: ${node}`);
790
+ const latestGateDecisions = new Map();
791
+ for (const decision of scope.gate_decisions) {
792
+ if (decision?.node === node
793
+ && (!decision.iteration_id || decision.iteration_id === iterationId)
794
+ && Number(decision.node_attempt || attempt) === attempt
795
+ && decision.gate_id) latestGateDecisions.set(decision.gate_id, decision);
796
+ }
797
+ const nonApproving = [...latestGateDecisions.values()].find((decision) =>
798
+ !new Set(["approved", "not-required"]).has(decision.decision));
799
+ if (nonApproving) {
800
+ throw new Error(`Node has a non-approving Gate decision ${nonApproving.gate_id}: ${nonApproving.decision}.`);
801
+ }
802
+ const missingGates = (NODE_RULES.get(node)?.gates || []).filter((gateId) => {
803
+ const decision = latestGateDecisions.get(gateId);
804
+ return !satisfiesMandatoryGate(node, decision);
805
+ });
806
+ if (missingGates.length) throw new Error(`Node is missing mandatory Gate decisions: ${missingGates.join(", ")}.`);
807
+ scope.active_nodes = scope.active_nodes.filter((item) => item !== node);
808
+ scope.completed_nodes = [...new Set([...scope.completed_nodes, node])];
809
+ } else if (event === "node-blocked") {
810
+ reason = requiredFlag(flags, "--reason");
811
+ if (!scope.active_nodes.includes(node)) throw new Error(`Node is not active: ${node}`);
812
+ scope.active_nodes = scope.active_nodes.filter((item) => item !== node);
813
+ scope.blocked_nodes = [...new Set([...scope.blocked_nodes, node])];
814
+ } else if (event === "node-unblocked") {
815
+ reason = requiredFlag(flags, "--reason");
816
+ if (!scope.blocked_nodes.includes(node)) throw new Error(`Node is not blocked: ${node}`);
817
+ scope.blocked_nodes = scope.blocked_nodes.filter((item) => item !== node);
818
+ scope.active_nodes = [...new Set([...scope.active_nodes, node])];
819
+ } else {
820
+ throw new Error(
821
+ "transition --event must be iteration-started, release-started, node-started, node-completed, "
822
+ + "node-blocked, node-unblocked, candidate-selected, release-selected, version-closed, or release-archived.",
823
+ );
824
+ }
825
+ return persist(scopeName, { node, ...(reason ? { reason } : {}) });
826
+ });
827
+ }
828
+
829
+ function registeredEvidence(files, evidence, options, node = null) {
830
+ if (!evidence.length) throw new Error("At least one --evidence artifact ID is required.");
831
+ const registry = normalizeArtifactRegistry(
832
+ readYaml(files.artifactRegistry, options.yaml, "artifact-registry.yaml"),
833
+ );
834
+ return evidence.map((artifactId) => {
835
+ const artifact = registry.artifacts.find((item) => (item.artifact_id || item.id) === artifactId);
836
+ if (!artifact) throw new Error(`Evidence artifact is not registered: ${artifactId}`);
837
+ if (node && artifact.node && artifact.node !== node) {
838
+ throw new Error(`Evidence ${artifactId} belongs to ${artifact.node}, not ${node}.`);
839
+ }
840
+ return artifact;
841
+ });
842
+ }
843
+
844
+ function requestGate(files, flags, options) {
845
+ const expected = expectedRevision(flags);
846
+ return withLock(files, "workflow-state", () => {
847
+ const state = normalizeWorkflowState(
848
+ readYaml(files.workflowState, options.yaml, "workflow-state.yaml"),
849
+ );
850
+ if (state.revision !== expected) {
851
+ throw new Error(`Workflow state revision conflict: expected ${expected}, actual ${state.revision}.`);
852
+ }
853
+ const gateId = requiredFlag(flags, "--gate-id");
854
+ const iterationId = requiredFlag(flags, "--iteration-id");
855
+ const node = requiredFlag(flags, "--node");
856
+ if (!WRITABLE_NODE_IDS.has(node)) {
857
+ if (NODE_IDS.has(node)) throw new Error(`Legacy workflow node is read-only: ${node}`);
858
+ throw new Error(`Unknown workflow node: ${node}`);
859
+ }
860
+ const scopeName = scopeNameForNode(node);
861
+ const scope = scopeState(state, scopeName);
862
+ if (scope.scope_id !== iterationId) {
863
+ throw new Error(`Iteration mismatch for ${scopeName} scope: state is ${scope.scope_id}, command is ${iterationId}.`);
864
+ }
865
+ if (!scope.active_nodes.includes(node)) throw new Error(`Gate node is not active: ${node}`);
866
+ const pending = scope.pending_gates.map((gate) =>
867
+ typeof gate === "string" ? { gate_id: gate, node: null } : gate);
868
+ if (pending.some((gate) => gate?.gate_id === gateId)) throw new Error(`Gate is already pending: ${gateId}`);
869
+ const evidence = repeatedFlag(flags, "--evidence");
870
+ registeredEvidence(files, evidence, options, node);
871
+
872
+ const timestamp = (options.now || (() => new Date().toISOString()))();
873
+ const request = {
874
+ gate_id: gateId,
875
+ node,
876
+ iteration_id: iterationId,
877
+ scope: scopeName,
878
+ node_attempt: Number(scope.node_attempts[node] || 1),
879
+ evidence,
880
+ requested_at: timestamp,
881
+ };
882
+ scope.pending_gates = [...pending, request];
883
+ const transition = {
884
+ event: "gate-requested",
885
+ scope: scopeName,
886
+ gate_id: gateId,
887
+ node,
888
+ iteration_id: iterationId,
889
+ evidence,
890
+ occurred_at: timestamp,
891
+ };
892
+ persistScopeMutation(files, state, scope, timestamp, transition, {
893
+ type: "gate-requested",
894
+ occurred_at: timestamp,
895
+ scope: scopeName,
896
+ gate_id: gateId,
897
+ node,
898
+ iteration_id: iterationId,
899
+ evidence,
900
+ }, options);
901
+ return {
902
+ command: "gate request",
903
+ revision: state.revision,
904
+ gate: request,
905
+ workflow_state: state,
906
+ };
907
+ });
908
+ }
909
+
910
+ function decideGate(files, flags, options) {
911
+ const expected = expectedRevision(flags);
912
+ return withLock(files, "workflow-state", () => {
913
+ const state = normalizeWorkflowState(
914
+ readYaml(files.workflowState, options.yaml, "workflow-state.yaml"),
915
+ );
916
+ if (state.revision !== expected) {
917
+ throw new Error(`Workflow state revision conflict: expected ${expected}, actual ${state.revision}.`);
918
+ }
919
+ const gateId = requiredFlag(flags, "--gate-id");
920
+ const scopes = [state.development_state, state.release_state];
921
+ let owningScope = null;
922
+ let pending = [];
923
+ let request = null;
924
+ for (const scope of scopes) {
925
+ const candidates = scope.pending_gates.map((gate) =>
926
+ typeof gate === "string" ? { gate_id: gate, node: null, evidence: [] } : gate);
927
+ const candidate = candidates.find((gate) => gate?.gate_id === gateId);
928
+ if (candidate) {
929
+ if (request) throw new Error(`Gate ID is pending in more than one scope: ${gateId}`);
930
+ request = candidate;
931
+ pending = candidates;
932
+ owningScope = scope;
933
+ }
934
+ }
935
+ if (!request) throw new Error(`Gate is not pending: ${gateId}`);
936
+ const scopeName = request.scope || scopeNameForNode(request.node);
937
+ const decision = requiredFlag(flags, "--decision").toLowerCase().replaceAll("_", "-");
938
+ if (!GATE_DECISIONS.has(decision)) throw new Error(`Unsupported gate decision: ${decision}`);
939
+ const actor = requiredFlag(flags, "--actor");
940
+ const rationale = requiredFlag(flags, "--rationale");
941
+ const evidence = repeatedFlag(flags, "--evidence");
942
+ const evidenceArtifacts = registeredEvidence(files, evidence, options);
943
+ const crossNodeEvidence = evidenceArtifacts.filter((artifact) => artifact.node !== request.node);
944
+ if (decision === "not-required") {
945
+ if (request.node !== "04-experience-design" || !UX_GATE_IDS.has(gateId)) {
946
+ throw new Error(`Gate ${gateId} cannot use not-required.`);
947
+ }
948
+ if (!evidenceArtifacts.some(isProductTerminalEvidence)) {
949
+ throw new Error(`UX Gate ${gateId} requires successful product-terminal evidence for not-required.`);
950
+ }
951
+ if (crossNodeEvidence.some((artifact) => !isProductTerminalEvidence(artifact))) {
952
+ throw new Error(`Gate evidence belongs to another node: ${crossNodeEvidence[0].artifact_id || crossNodeEvidence[0].id}.`);
953
+ }
954
+ } else if (crossNodeEvidence.length) {
955
+ throw new Error(`Gate evidence belongs to another node: ${crossNodeEvidence[0].artifact_id || crossNodeEvidence[0].id}.`);
956
+ }
957
+ for (const requestedEvidence of request.evidence || []) {
958
+ if (!evidence.includes(requestedEvidence)) {
959
+ throw new Error(`Gate decision must retain requested evidence: ${requestedEvidence}`);
960
+ }
961
+ }
962
+
963
+ const timestamp = (options.now || (() => new Date().toISOString()))();
964
+ const record = {
965
+ gate_id: gateId,
966
+ node: request.node,
967
+ iteration_id: request.iteration_id || owningScope.scope_id,
968
+ scope: scopeName,
969
+ node_attempt: request.node_attempt || Number(owningScope.node_attempts[request.node] || 1),
970
+ decision,
971
+ actor,
972
+ rationale,
973
+ evidence,
974
+ decided_at: timestamp,
975
+ };
976
+ owningScope.pending_gates = pending.filter((gate) => gate?.gate_id !== gateId);
977
+ owningScope.gate_decisions = [...owningScope.gate_decisions, record];
978
+ const transition = {
979
+ event: "gate-decided",
980
+ scope: scopeName,
981
+ ...record,
982
+ occurred_at: timestamp,
983
+ };
984
+ persistScopeMutation(files, state, owningScope, timestamp, transition, {
985
+ type: "gate-decided",
986
+ occurred_at: timestamp,
987
+ scope: scopeName,
988
+ ...record,
989
+ }, options);
990
+ return {
991
+ command: "gate decide",
992
+ revision: state.revision,
993
+ gate: record,
994
+ workflow_state: state,
995
+ };
996
+ });
997
+ }
998
+
999
+ function readEvents(files, errors) {
1000
+ if (!fs.existsSync(files.events)) return [];
1001
+ const events = [];
1002
+ const lines = fs.readFileSync(files.events, "utf8").split(/\r?\n/);
1003
+ for (let index = 0; index < lines.length; index += 1) {
1004
+ if (!lines[index].trim()) continue;
1005
+ try {
1006
+ const event = JSON.parse(lines[index]);
1007
+ if (!event || typeof event !== "object" || Array.isArray(event)) {
1008
+ errors.push(`Audit event at line ${index + 1} must be a JSON object.`);
1009
+ } else {
1010
+ events.push(event);
1011
+ }
1012
+ } catch {
1013
+ errors.push(`Audit event at line ${index + 1} is not valid JSON.`);
1014
+ }
1015
+ }
1016
+ return events;
1017
+ }
1018
+
1019
+ function verifyStream(file, value, stream, events, errors, allowedSchemas = new Set([1])) {
1020
+ if (!allowedSchemas.has(value.schema_version)) {
1021
+ errors.push(`${stream} schema_version must be one of ${[...allowedSchemas].join(", ")}.`);
1022
+ }
1023
+ if (!Number.isInteger(value.revision) || value.revision < 0) {
1024
+ errors.push(`${stream} revision must be a non-negative integer.`);
1025
+ return;
1026
+ }
1027
+ const streamEvents = events.filter((event) => event.stream === stream);
1028
+ if (value.revision === 0) {
1029
+ if (streamEvents.length) errors.push(`${stream} revision is 0 but audit events exist.`);
1030
+ return;
1031
+ }
1032
+ if (streamEvents.length !== value.revision) {
1033
+ errors.push(
1034
+ `Audit event count mismatch for ${stream}: expected ${value.revision}, actual ${streamEvents.length}.`,
1035
+ );
1036
+ }
1037
+ const revisions = streamEvents.map((event) => event.revision);
1038
+ for (let revision = 1; revision <= value.revision; revision += 1) {
1039
+ if (revisions.filter((item) => item === revision).length !== 1) {
1040
+ errors.push(`${stream} audit revision ${revision} must occur exactly once.`);
1041
+ }
1042
+ }
1043
+ const latest = streamEvents.find((event) => event.revision === value.revision);
1044
+ if (!latest) return;
1045
+ const checksum = sha256(fs.readFileSync(file));
1046
+ if (latest.after_checksum !== checksum) {
1047
+ errors.push(`Audit checksum mismatch for ${stream}.`);
1048
+ }
1049
+ }
1050
+
1051
+ function verifyFormalState(files, options) {
1052
+ const project = readYaml(files.project, options.yaml, "project.yaml");
1053
+ const rawState = readYaml(files.workflowState, options.yaml, "workflow-state.yaml");
1054
+ const rawRegistry = readYaml(files.artifactRegistry, options.yaml, "artifact-registry.yaml");
1055
+ const state = normalizeWorkflowState(rawState);
1056
+ const registry = normalizeArtifactRegistry(rawRegistry);
1057
+ const errors = [];
1058
+ const events = readEvents(files, errors);
1059
+ let shards = [];
1060
+ try {
1061
+ shards = readEventShards(files, options);
1062
+ } catch (error) {
1063
+ errors.push(error.message);
1064
+ }
1065
+
1066
+ verifyStream(files.workflowState, rawState, "workflow-state", events, errors, new Set([1, 2]));
1067
+ verifyStream(files.artifactRegistry, rawRegistry, "artifact-registry", events, errors);
1068
+ if (shards.length) {
1069
+ for (const [stream, value] of [["workflow-state", rawState], ["artifact-registry", rawRegistry]]) {
1070
+ const latest = shards.filter((record) => record.stream === stream)
1071
+ .sort((left, right) => right.revision - left.revision)[0];
1072
+ if (latest && (latest.revision !== value.revision
1073
+ || latest.after_checksum !== sha256(options.yaml.stringify(value)))) {
1074
+ errors.push(`Derived view does not match latest authoritative shard for ${stream}.`);
1075
+ }
1076
+ }
1077
+ }
1078
+ if (project.project_id && state.project_id && project.project_id !== state.project_id) {
1079
+ errors.push(`Project ID mismatch: project.yaml=${project.project_id}, workflow-state=${state.project_id}.`);
1080
+ }
1081
+ if (project.current_iteration_id && state.development_state.scope_id
1082
+ && project.current_iteration_id !== state.development_state.scope_id) {
1083
+ errors.push(
1084
+ `Iteration mismatch: project.yaml=${project.current_iteration_id}, workflow-state=${state.development_state.scope_id}.`,
1085
+ );
1086
+ }
1087
+ if (project.current_release_id && state.release_state.scope_id
1088
+ && project.current_release_id !== state.release_state.scope_id) {
1089
+ errors.push(
1090
+ `Release mismatch: project.yaml=${project.current_release_id}, workflow-state=${state.release_state.scope_id}.`,
1091
+ );
1092
+ }
1093
+
1094
+ for (const [scopeName, scope] of [["development", state.development_state], ["release", state.release_state]]) {
1095
+ const active = new Set(scope.active_nodes);
1096
+ const blocked = new Set(scope.blocked_nodes);
1097
+ const completed = new Set(scope.completed_nodes);
1098
+ const hasScopedState = active.size || blocked.size || completed.size
1099
+ || scope.pending_gates.length || scope.gate_decisions.length
1100
+ || Object.keys(scope.node_attempts).length || scope.latest_candidate_id || scope.latest_release_id;
1101
+ if (hasScopedState && !scope.scope_id) {
1102
+ errors.push(`Workflow ${scopeName} scope has state but no scope_id.`);
1103
+ }
1104
+ for (const node of active) {
1105
+ if (blocked.has(node) || completed.has(node)) {
1106
+ errors.push(`Workflow node appears in incompatible states in ${scopeName}: ${node}.`);
1107
+ }
1108
+ }
1109
+ for (const node of blocked) {
1110
+ if (completed.has(node)) errors.push(`Workflow node appears in incompatible states in ${scopeName}: ${node}.`);
1111
+ }
1112
+ for (const node of [...active, ...blocked, ...completed]) {
1113
+ if (!NODE_IDS.has(node)) errors.push(`Unknown workflow node in formal state: ${node}.`);
1114
+ else if (scopeNameForNode(node) !== scopeName) {
1115
+ errors.push(`Workflow node is stored in the wrong ${scopeName} scope: ${node}.`);
1116
+ }
1117
+ }
1118
+ }
1119
+
1120
+ const artifactIds = new Set();
1121
+ const artifactPaths = new Set();
1122
+ for (const artifact of registry.artifacts) {
1123
+ const artifactId = artifact.artifact_id || artifact.id;
1124
+ if (!artifactId) {
1125
+ errors.push("Registered artifact is missing artifact_id.");
1126
+ continue;
1127
+ }
1128
+ if (artifactIds.has(artifactId)) errors.push(`Duplicate artifact ID: ${artifactId}.`);
1129
+ artifactIds.add(artifactId);
1130
+
1131
+ const artifactPath = typeof artifact.path === "string" ? artifact.path.replaceAll("\\", "/") : "";
1132
+ if (!artifactPath.startsWith(".workflow/")) {
1133
+ errors.push(`Artifact path must start with .workflow/: ${artifactId}.`);
1134
+ continue;
1135
+ }
1136
+ if (artifactPaths.has(artifactPath)) errors.push(`Duplicate artifact path: ${artifactPath}.`);
1137
+ artifactPaths.add(artifactPath);
1138
+
1139
+ const absolute = path.resolve(files.root, artifactPath);
1140
+ if (!isInside(absolute, files.workflow) || !fs.existsSync(absolute) || !fs.statSync(absolute).isFile()) {
1141
+ errors.push(`Artifact file is missing or outside .workflow/: ${artifactId}.`);
1142
+ continue;
1143
+ }
1144
+ if (!isInside(fs.realpathSync(absolute), fs.realpathSync(files.workflow))) {
1145
+ errors.push(`Artifact path resolves outside .workflow/: ${artifactId}.`);
1146
+ continue;
1147
+ }
1148
+ const checksum = sha256(fs.readFileSync(absolute));
1149
+ if (artifact.checksum_algorithm !== "sha256" || artifact.checksum !== checksum) {
1150
+ errors.push(`Checksum mismatch for artifact ${artifactId}.`);
1151
+ }
1152
+ }
1153
+
1154
+ if (errors.length) throw new Error(`Formal state verification failed:\n- ${errors.join("\n- ")}`);
1155
+ return {
1156
+ command: "verify",
1157
+ valid: true,
1158
+ errors: [],
1159
+ workflow_revision: state.revision,
1160
+ artifact_registry_revision: registry.revision,
1161
+ artifact_count: registry.artifacts.length,
1162
+ audit_event_count: events.length,
1163
+ event_shard_count: shards.length,
1164
+ };
1165
+ }
1166
+
1167
+ export function executeDeliveryStateCommand(argv, options = {}) {
1168
+ requireYaml(options.yaml);
1169
+ const { positional, options: flags } = parseArgs(argv);
1170
+ const command = positional[0];
1171
+ const projectRoot = path.resolve(String(flags.get("--project") || options.projectRoot || process.cwd()));
1172
+ const files = statePaths(projectRoot);
1173
+ const descriptor = lifecycleDescriptor(command, positional, flags);
1174
+ const before = dispatchLifecycle(options, files, descriptor, "before", flags);
1175
+ if (before.decision === "block") {
1176
+ const error = new Error(`Evolution Hook blocked ${descriptor.action}: ${(before.messages || []).join("; ") || "no rationale"}`);
1177
+ dispatchLifecycle(options, files, descriptor, "failed", flags, null, error);
1178
+ throw error;
1179
+ }
1180
+ try {
1181
+ let result;
1182
+ if (command === "artifact" && positional[1] === "register") {
1183
+ result = registerArtifact(files, flags, options);
1184
+ } else if (command === "artifact" && positional[1] === "status") {
1185
+ result = changeArtifactStatus(files, flags, options);
1186
+ } else if (command === "gate" && positional[1] === "request") {
1187
+ result = requestGate(files, flags, options);
1188
+ } else if (command === "gate" && positional[1] === "decide") {
1189
+ result = decideGate(files, flags, options);
1190
+ } else if (command === "transition") {
1191
+ result = transitionWorkflow(files, flags, options);
1192
+ } else if (command === "verify") {
1193
+ result = verifyFormalState(files, options);
1194
+ } else if (command === "rebuild") {
1195
+ result = rebuildFormalViews(files, options);
1196
+ } else if (command === "inspect") {
1197
+ const project = readYaml(files.project, options.yaml, "project.yaml");
1198
+ const workflowState = normalizeWorkflowState(
1199
+ readYaml(files.workflowState, options.yaml, "workflow-state.yaml"),
1200
+ );
1201
+ const artifactRegistry = normalizeArtifactRegistry(
1202
+ readYaml(files.artifactRegistry, options.yaml, "artifact-registry.yaml"),
1203
+ );
1204
+ result = {
1205
+ command,
1206
+ project_id: project.project_id || workflowState.project_id || path.basename(projectRoot),
1207
+ project,
1208
+ workflow_state: workflowState,
1209
+ artifact_registry: artifactRegistry,
1210
+ };
1211
+ } else {
1212
+ throw new Error("Use inspect, verify, rebuild, artifact register/status, transition, or gate request/decide.");
1213
+ }
1214
+ dispatchLifecycle(options, files, descriptor, "after", flags, result);
1215
+ return result;
1216
+ } catch (error) {
1217
+ dispatchLifecycle(options, files, descriptor, "failed", flags, null, error);
1218
+ throw error;
1219
+ }
1220
+ }