ai-delivery-workflow 0.2.2 → 0.4.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 (361) hide show
  1. package/README.md +45 -14
  2. package/bin/ai-delivery.mjs +181 -6
  3. package/bin/benchmark-autonomy.mjs +217 -0
  4. package/bin/release-check.mjs +382 -0
  5. package/docs/CLI-PARAMETER-REFERENCE.zh-CN.md +346 -0
  6. package/docs/CODEGRAPH-INTEGRATION.zh-CN.md +89 -0
  7. package/docs/DUAL-REPOSITORY-WORKSPACE.zh-CN.md +2 -2
  8. package/docs/EVOLUTION-MAINTENANCE.zh-CN.md +7 -7
  9. package/docs/EVOLUTION-USER-GUIDE.zh-CN.md +2 -2
  10. package/docs/FILE-REFERENCE.zh-CN.md +120 -39
  11. package/docs/MAINTENANCE-VERIFICATION.zh-CN.md +51 -0
  12. package/docs/MOCK-SYSTEM.zh-CN.md +141 -0
  13. package/docs/NPM-RELEASE-MANAGEMENT.zh-CN.md +106 -0
  14. package/docs/PROJECT-MANUAL.zh-CN.md +154 -95
  15. package/docs/PROTOTYPE-MIGRATION.zh-CN.md +164 -0
  16. package/docs/STATE-CLI-MAINTENANCE.zh-CN.md +35 -17
  17. package/docs/STATE-CLI-USER-GUIDE.zh-CN.md +115 -39
  18. package/docs/VIEWER-MAINTENANCE.zh-CN.md +101 -324
  19. package/docs/VIEWER-USER-GUIDE.zh-CN.md +68 -190
  20. package/docs/adr/0001-prototype-first-ui-governance.md +3 -0
  21. package/docs/adr/0002-microcks-as-default-mock-system.md +3 -0
  22. package/docs/adr/0003-workflow-manager-governed-control-surface.md +46 -0
  23. package/docs/adr/0004-operational-carbon-visual-standard.md +9 -0
  24. package/docs/adr/0005-prototype-management-as-routed-pages.md +7 -0
  25. package/docs/agents/domain.md +12 -0
  26. package/docs/agents/issue-tracker.md +27 -0
  27. package/docs/agents/triage-labels.md +11 -0
  28. package/docs/agents/workflow-manager-acceptance-matrix.md +93 -0
  29. package/docs/agents/workflow-manager-agent-interface.md +145 -0
  30. package/docs/agents/workflow-manager-current-state.md +205 -0
  31. package/lib/artifact-files.mjs +47 -0
  32. package/lib/codegraph.mjs +405 -0
  33. package/lib/delivery-state.mjs +3079 -198
  34. package/lib/frontend-guard.mjs +288 -0
  35. package/lib/fs-utils.mjs +53 -0
  36. package/lib/history-anchor.mjs +98 -0
  37. package/lib/mock-scenario-pack.mjs +687 -0
  38. package/lib/mock-system.mjs +897 -0
  39. package/lib/npm-package-result.mjs +11 -0
  40. package/lib/png-evidence.mjs +149 -0
  41. package/lib/project-bootstrap.mjs +96 -55
  42. package/lib/project-installer.mjs +1484 -164
  43. package/lib/project-migration.mjs +1193 -0
  44. package/lib/prototype-migration.mjs +713 -0
  45. package/lib/prototype-page-review.mjs +384 -0
  46. package/lib/prototype-review-target.mjs +334 -0
  47. package/lib/prototype-revision.mjs +812 -0
  48. package/lib/stable-value.mjs +15 -0
  49. package/lib/toml-hooks.mjs +69 -0
  50. package/lib/ui-acceptance.mjs +565 -0
  51. package/lib/visual-standard.mjs +232 -0
  52. package/lib/workflow-manager-contracts.mjs +367 -0
  53. package/lib/workflow-manager-facts.mjs +778 -0
  54. package/lib/workflow-manager-governance.mjs +514 -0
  55. package/lib/workflow-manager-mock.mjs +183 -0
  56. package/lib/workflow-manager-prototypes.mjs +1847 -0
  57. package/lib/workflow-manager-public/assets/ibm-plex-mono-latin-400-normal-CvHOgSBP.woff +0 -0
  58. package/lib/workflow-manager-public/assets/ibm-plex-mono-latin-400-normal-DMJ8VG8y.woff2 +0 -0
  59. package/lib/workflow-manager-public/assets/ibm-plex-sans-latin-400-normal-CDDApCn2.woff2 +0 -0
  60. package/lib/workflow-manager-public/assets/ibm-plex-sans-latin-400-normal-CYLoc0-x.woff +0 -0
  61. package/lib/workflow-manager-public/assets/ibm-plex-sans-latin-500-normal-6ng42L7E.woff2 +0 -0
  62. package/lib/workflow-manager-public/assets/ibm-plex-sans-latin-500-normal-BgVn5rGT.woff +0 -0
  63. package/lib/workflow-manager-public/assets/ibm-plex-sans-latin-600-normal-Cu4Hd6ag.woff +0 -0
  64. package/lib/workflow-manager-public/assets/ibm-plex-sans-latin-600-normal-CuJfVYMP.woff2 +0 -0
  65. package/lib/workflow-manager-public/assets/index-Chlg6r8f.css +1 -0
  66. package/lib/workflow-manager-public/assets/index-E-hGB5ZD.js +13 -0
  67. package/lib/workflow-manager-public/fonts/OFL-1.1.txt +90 -0
  68. package/lib/workflow-manager-public/index.html +14 -0
  69. package/lib/workflow-manager-systems.mjs +519 -0
  70. package/lib/workflow-manager-transaction.mjs +397 -0
  71. package/lib/workflow-manager.mjs +745 -0
  72. package/package.json +8 -2
  73. package/skills/ai-delivery-assemble-release/agents/openai.yaml +3 -3
  74. package/skills/ai-delivery-assemble-release/assets/release-material-index.csv +1 -1
  75. package/skills/ai-delivery-assemble-release/assets/version-inclusion.csv +1 -1
  76. package/skills/ai-delivery-bootstrap/SKILL.md +4 -4
  77. package/skills/ai-delivery-bootstrap/agents/openai.yaml +3 -3
  78. package/skills/ai-delivery-bootstrap/references/bootstrap-contract.md +4 -2
  79. package/skills/ai-delivery-checkpoint-task/SKILL.md +17 -3
  80. package/skills/ai-delivery-checkpoint-task/agents/openai.yaml +3 -3
  81. package/skills/ai-delivery-checkpoint-task/assets/runtime-template/task.json +7 -1
  82. package/skills/ai-delivery-checkpoint-task/references/checkpoint-contract.md +22 -8
  83. package/skills/ai-delivery-checkpoint-task/scripts/hook-event.mjs +91 -0
  84. package/skills/ai-delivery-checkpoint-task/scripts/task-state.mjs +1086 -51
  85. package/skills/ai-delivery-close-version/SKILL.md +9 -8
  86. package/skills/ai-delivery-close-version/agents/openai.yaml +3 -3
  87. package/skills/ai-delivery-close-version/assets/version-lineage.yaml +1 -1
  88. package/skills/ai-delivery-close-version/references/version-closeout-contract.md +2 -2
  89. package/skills/ai-delivery-define-product/SKILL.md +8 -4
  90. package/skills/ai-delivery-define-product/agents/openai.yaml +4 -4
  91. package/skills/ai-delivery-define-product/references/product-contract.md +2 -2
  92. package/skills/ai-delivery-deploy-production/agents/openai.yaml +3 -3
  93. package/skills/ai-delivery-design-architecture/SKILL.md +9 -5
  94. package/skills/ai-delivery-design-architecture/assets/architecture-template/prototype-architecture-validation.yaml +16 -16
  95. package/skills/ai-delivery-design-architecture/references/architecture-contract.md +1 -1
  96. package/skills/ai-delivery-design-experience/SKILL.md +36 -30
  97. package/skills/ai-delivery-design-experience/agents/openai.yaml +4 -4
  98. package/skills/ai-delivery-design-experience/assets/experience-template/LEGACY-COMPATIBILITY.md +16 -0
  99. package/skills/ai-delivery-design-experience/assets/experience-template/design-reference-selection.yaml +32 -0
  100. package/skills/ai-delivery-design-experience/assets/experience-template/experience-baseline.yaml +2 -2
  101. package/skills/ai-delivery-design-experience/assets/experience-template/formal-ui-confirmation.yaml +16 -3
  102. package/skills/ai-delivery-design-experience/assets/experience-template/frontend-change-scope.yaml +30 -0
  103. package/skills/ai-delivery-design-experience/assets/experience-template/interaction-contract.csv +1 -1
  104. package/skills/ai-delivery-design-experience/assets/experience-template/mock-admin-export.yaml +8 -0
  105. package/skills/ai-delivery-design-experience/assets/experience-template/mock-scenario-mappings.csv +1 -0
  106. package/skills/ai-delivery-design-experience/assets/experience-template/mock-scenario-pack-request.yaml +24 -0
  107. package/skills/ai-delivery-design-experience/assets/experience-template/product-prototype-reconciliation.yaml +2 -1
  108. package/skills/ai-delivery-design-experience/assets/experience-template/prototype-adjustment-log.csv +1 -1
  109. package/skills/ai-delivery-design-experience/assets/experience-template/prototype-component-bindings.csv +1 -0
  110. package/skills/ai-delivery-design-experience/assets/experience-template/prototype-component-candidates.csv +1 -0
  111. package/skills/ai-delivery-design-experience/assets/experience-template/prototype-components.csv +1 -0
  112. package/skills/ai-delivery-design-experience/assets/experience-template/prototype-confirmation.yaml +18 -18
  113. package/skills/ai-delivery-design-experience/assets/experience-template/prototype-data-contracts.csv +1 -0
  114. package/skills/ai-delivery-design-experience/assets/experience-template/prototype-impact-assessment.yaml +5 -5
  115. package/skills/ai-delivery-design-experience/assets/experience-template/prototype-implementation-bindings.csv +1 -0
  116. package/skills/ai-delivery-design-experience/assets/experience-template/prototype-manifest.yaml +2 -0
  117. package/skills/ai-delivery-design-experience/assets/experience-template/prototype-pages.csv +1 -0
  118. package/skills/ai-delivery-design-experience/assets/experience-template/prototype-product-coverage.csv +1 -1
  119. package/skills/ai-delivery-design-experience/assets/experience-template/prototype-review-comments.csv +1 -1
  120. package/skills/ai-delivery-design-experience/assets/experience-template/prototype-review-decision.yaml +13 -12
  121. package/skills/ai-delivery-design-experience/assets/experience-template/prototype-review-scenes.csv +1 -0
  122. package/skills/ai-delivery-design-experience/assets/experience-template/prototype-review-sessions.csv +1 -1
  123. package/skills/ai-delivery-design-experience/assets/experience-template/prototype-review-unit-pages.csv +1 -0
  124. package/skills/ai-delivery-design-experience/assets/experience-template/prototype-review-units.csv +1 -0
  125. package/skills/ai-delivery-design-experience/assets/experience-template/prototype-revision.yaml +88 -0
  126. package/skills/ai-delivery-design-experience/assets/experience-template/prototype-routes.csv +1 -0
  127. package/skills/ai-delivery-design-experience/assets/experience-template/prototype-set.yaml +2 -0
  128. package/skills/ai-delivery-design-experience/assets/experience-template/prototype-source-inspection-snapshots.csv +1 -0
  129. package/skills/ai-delivery-design-experience/assets/experience-template/prototype-terminals.csv +1 -0
  130. package/skills/ai-delivery-design-experience/assets/experience-template/prototype-tool-candidates.csv +1 -1
  131. package/skills/ai-delivery-design-experience/assets/experience-template/prototype-tool-selection.yaml +2 -3
  132. package/skills/ai-delivery-design-experience/assets/experience-template/prototype-traceability.csv +1 -1
  133. package/skills/ai-delivery-design-experience/assets/experience-template/prototype-trigger-conditions.csv +1 -0
  134. package/skills/ai-delivery-design-experience/assets/experience-template/prototype.yaml +6 -0
  135. package/skills/ai-delivery-design-experience/assets/experience-template/ui-framework-selection.yaml +17 -1
  136. package/skills/ai-delivery-design-experience/assets/experience-template/ui-handoff.yaml +6 -5
  137. package/skills/ai-delivery-design-experience/assets/experience-template/ui-mapping.csv +1 -0
  138. package/skills/ai-delivery-design-experience/assets/experience-template/visual-direction.yaml +4 -0
  139. package/skills/ai-delivery-design-experience/assets/experience-template/visual-reference-manifest.yaml +23 -0
  140. package/skills/ai-delivery-design-experience/assets/experience-template/visual-reference.html +163 -0
  141. package/skills/ai-delivery-design-experience/assets/experience-template/visual-system-confirmation.yaml +11 -0
  142. package/skills/ai-delivery-design-experience/references/experience-contract.md +26 -27
  143. package/skills/ai-delivery-design-experience/references/prototype-management-contract.md +42 -21
  144. package/skills/ai-delivery-design-tests/SKILL.md +13 -7
  145. package/skills/ai-delivery-design-tests/assets/test-design-template/prototype-test-scope.csv +1 -1
  146. package/skills/ai-delivery-design-tests/assets/test-design-template/prototype-test-validation.yaml +17 -16
  147. package/skills/ai-delivery-design-tests/references/test-design-contract.md +7 -5
  148. package/skills/ai-delivery-develop-iteration/SKILL.md +18 -11
  149. package/skills/ai-delivery-develop-iteration/agents/openai.yaml +3 -3
  150. package/skills/ai-delivery-develop-iteration/assets/development-template/candidate-manifest.yaml +36 -0
  151. package/skills/ai-delivery-develop-iteration/assets/development-template/task-prototype-traceability.csv +1 -1
  152. package/skills/ai-delivery-develop-iteration/references/development-contract.md +5 -3
  153. package/skills/ai-delivery-evolve-workflow/agents/openai.yaml +3 -3
  154. package/skills/ai-delivery-execute-test/SKILL.md +6 -2
  155. package/skills/ai-delivery-execute-test/agents/openai.yaml +4 -4
  156. package/skills/ai-delivery-execute-work-package/SKILL.md +10 -3
  157. package/skills/ai-delivery-execute-work-package/agents/openai.yaml +4 -4
  158. package/skills/ai-delivery-execute-work-package/references/work-package-contract.md +1 -1
  159. package/skills/ai-delivery-manage-git/agents/openai.yaml +3 -3
  160. package/skills/ai-delivery-manage-standards/agents/openai.yaml +4 -4
  161. package/skills/ai-delivery-orchestrate/SKILL.md +47 -29
  162. package/skills/ai-delivery-orchestrate/agents/openai.yaml +3 -3
  163. package/skills/ai-delivery-orchestrate/assets/project-template/.workflow/control/migrations/.gitkeep +0 -0
  164. package/skills/ai-delivery-orchestrate/assets/project-template/.workflow/control/repairs/.gitkeep +0 -0
  165. package/skills/ai-delivery-orchestrate/assets/project-template/.workflow/delivery/project.yaml +1 -0
  166. package/skills/ai-delivery-orchestrate/assets/project-template/.workflow/delivery/workflow-state.yaml +2 -0
  167. package/skills/ai-delivery-orchestrate/assets/project-template/.workflow/tools/frontend-guard/frontend-guard.mjs +50 -0
  168. package/skills/ai-delivery-orchestrate/assets/project-template/.workflow/tools/hooks/hook-event.mjs +91 -0
  169. package/skills/ai-delivery-orchestrate/assets/project-template/.workflow/tools/manager/manager.mjs +64 -0
  170. package/skills/ai-delivery-orchestrate/assets/project-template/.workflow/tools/manager/start.cmd +4 -0
  171. package/skills/ai-delivery-orchestrate/assets/project-template/.workflow/tools/manager/start.ps1 +2 -0
  172. package/skills/ai-delivery-orchestrate/assets/project-template/.workflow/tools/{viewer → manager}/start.sh +1 -1
  173. package/skills/ai-delivery-orchestrate/assets/project-template/.workflow/tools/mock/mock.mjs +50 -0
  174. package/skills/ai-delivery-orchestrate/assets/project-template/.workflow/tools/ui-acceptance/ui-acceptance.mjs +37 -0
  175. package/skills/ai-delivery-orchestrate/assets/project-template/AGENTS.md +32 -6
  176. package/skills/ai-delivery-orchestrate/assets/project-template/gitignore.template +1 -0
  177. package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/CONTRIBUTING.md +25 -0
  178. package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/LICENSE +21 -0
  179. package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/README.md +250 -0
  180. package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/UPSTREAM.yaml +19 -0
  181. package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/airbnb/DESIGN.md +545 -0
  182. package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/airbnb/README.md +5 -0
  183. package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/airtable/DESIGN.md +554 -0
  184. package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/airtable/README.md +5 -0
  185. package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/apple/DESIGN.md +562 -0
  186. package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/apple/README.md +5 -0
  187. package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/binance/DESIGN.md +634 -0
  188. package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/binance/README.md +5 -0
  189. package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/bmw/DESIGN.md +544 -0
  190. package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/bmw/README.md +5 -0
  191. package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/bmw-m/DESIGN.md +503 -0
  192. package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/bmw-m/README.md +5 -0
  193. package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/bugatti/DESIGN.md +454 -0
  194. package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/bugatti/README.md +5 -0
  195. package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/cal/DESIGN.md +542 -0
  196. package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/cal/README.md +5 -0
  197. package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/claude/DESIGN.md +589 -0
  198. package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/claude/README.md +5 -0
  199. package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/clay/DESIGN.md +541 -0
  200. package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/clay/README.md +5 -0
  201. package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/clickhouse/DESIGN.md +544 -0
  202. package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/clickhouse/README.md +5 -0
  203. package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/cohere/DESIGN.md +451 -0
  204. package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/cohere/README.md +5 -0
  205. package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/coinbase/DESIGN.md +570 -0
  206. package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/coinbase/README.md +5 -0
  207. package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/composio/DESIGN.md +506 -0
  208. package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/composio/README.md +5 -0
  209. package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/cursor/DESIGN.md +537 -0
  210. package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/cursor/README.md +5 -0
  211. package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/dell-1996/DESIGN.md +632 -0
  212. package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/dell-1996/README.md +5 -0
  213. package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/elevenlabs/DESIGN.md +504 -0
  214. package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/elevenlabs/README.md +5 -0
  215. package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/expo/DESIGN.md +526 -0
  216. package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/expo/README.md +5 -0
  217. package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/ferrari/DESIGN.md +531 -0
  218. package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/ferrari/README.md +5 -0
  219. package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/figma/DESIGN.md +578 -0
  220. package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/figma/README.md +5 -0
  221. package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/framer/DESIGN.md +544 -0
  222. package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/framer/README.md +5 -0
  223. package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/hashicorp/DESIGN.md +575 -0
  224. package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/hashicorp/README.md +5 -0
  225. package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/hp/DESIGN.md +670 -0
  226. package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/hp/README.md +5 -0
  227. package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/ibm/DESIGN.md +550 -0
  228. package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/ibm/README.md +5 -0
  229. package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/intercom/DESIGN.md +546 -0
  230. package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/intercom/README.md +5 -0
  231. package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/kraken/DESIGN.md +125 -0
  232. package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/kraken/README.md +5 -0
  233. package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/lamborghini/DESIGN.md +288 -0
  234. package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/lamborghini/README.md +5 -0
  235. package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/linear.app/DESIGN.md +548 -0
  236. package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/linear.app/README.md +5 -0
  237. package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/lovable/DESIGN.md +298 -0
  238. package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/lovable/README.md +5 -0
  239. package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/mastercard/DESIGN.md +365 -0
  240. package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/mastercard/README.md +5 -0
  241. package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/meta/DESIGN.md +683 -0
  242. package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/meta/README.md +5 -0
  243. package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/minimax/DESIGN.md +746 -0
  244. package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/minimax/README.md +5 -0
  245. package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/mintlify/DESIGN.md +852 -0
  246. package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/mintlify/README.md +5 -0
  247. package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/miro/DESIGN.md +825 -0
  248. package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/miro/README.md +5 -0
  249. package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/mistral.ai/DESIGN.md +773 -0
  250. package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/mistral.ai/README.md +5 -0
  251. package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/mongodb/DESIGN.md +767 -0
  252. package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/mongodb/README.md +5 -0
  253. package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/nike/DESIGN.md +575 -0
  254. package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/nike/README.md +5 -0
  255. package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/nintendo-2001/DESIGN.md +649 -0
  256. package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/nintendo-2001/README.md +5 -0
  257. package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/notion/DESIGN.md +821 -0
  258. package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/notion/README.md +5 -0
  259. package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/nvidia/DESIGN.md +640 -0
  260. package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/nvidia/README.md +5 -0
  261. package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/ollama/DESIGN.md +539 -0
  262. package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/ollama/README.md +5 -0
  263. package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/opencode.ai/DESIGN.md +521 -0
  264. package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/opencode.ai/README.md +5 -0
  265. package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/pinterest/DESIGN.md +597 -0
  266. package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/pinterest/README.md +5 -0
  267. package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/playstation/DESIGN.md +661 -0
  268. package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/playstation/README.md +5 -0
  269. package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/posthog/DESIGN.md +690 -0
  270. package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/posthog/README.md +5 -0
  271. package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/raycast/DESIGN.md +669 -0
  272. package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/raycast/README.md +5 -0
  273. package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/renault/DESIGN.md +589 -0
  274. package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/renault/README.md +5 -0
  275. package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/replicate/DESIGN.md +616 -0
  276. package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/replicate/README.md +5 -0
  277. package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/resend/DESIGN.md +585 -0
  278. package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/resend/README.md +5 -0
  279. package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/revolut/DESIGN.md +636 -0
  280. package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/revolut/README.md +5 -0
  281. package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/runwayml/DESIGN.md +244 -0
  282. package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/runwayml/README.md +5 -0
  283. package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/sanity/DESIGN.md +357 -0
  284. package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/sanity/README.md +5 -0
  285. package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/sentry/DESIGN.md +551 -0
  286. package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/sentry/README.md +5 -0
  287. package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/shopify/DESIGN.md +516 -0
  288. package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/shopify/README.md +5 -0
  289. package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/slack/DESIGN.md +482 -0
  290. package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/spacex/DESIGN.md +363 -0
  291. package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/spacex/README.md +5 -0
  292. package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/spotify/DESIGN.md +246 -0
  293. package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/spotify/README.md +5 -0
  294. package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/starbucks/DESIGN.md +580 -0
  295. package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/starbucks/README.md +5 -0
  296. package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/stripe/DESIGN.md +487 -0
  297. package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/stripe/README.md +5 -0
  298. package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/supabase/DESIGN.md +462 -0
  299. package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/supabase/README.md +5 -0
  300. package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/superhuman/DESIGN.md +448 -0
  301. package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/superhuman/README.md +5 -0
  302. package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/tesla/DESIGN.md +286 -0
  303. package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/tesla/README.md +5 -0
  304. package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/theverge/DESIGN.md +339 -0
  305. package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/theverge/README.md +5 -0
  306. package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/together.ai/DESIGN.md +633 -0
  307. package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/together.ai/README.md +5 -0
  308. package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/uber/DESIGN.md +636 -0
  309. package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/uber/README.md +5 -0
  310. package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/vercel/DESIGN.md +736 -0
  311. package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/vercel/README.md +5 -0
  312. package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/vodafone/DESIGN.md +538 -0
  313. package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/vodafone/README.md +5 -0
  314. package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/voltagent/DESIGN.md +521 -0
  315. package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/voltagent/README.md +5 -0
  316. package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/warp/DESIGN.md +526 -0
  317. package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/warp/README.md +5 -0
  318. package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/webflow/DESIGN.md +588 -0
  319. package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/webflow/README.md +5 -0
  320. package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/wired/DESIGN.md +497 -0
  321. package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/wired/README.md +5 -0
  322. package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/wise/DESIGN.md +544 -0
  323. package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/wise/README.md +5 -0
  324. package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/x.ai/DESIGN.md +465 -0
  325. package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/x.ai/README.md +5 -0
  326. package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/zapier/DESIGN.md +537 -0
  327. package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/zapier/README.md +5 -0
  328. package/skills/ai-delivery-orchestrate/references/formal-state-contract.md +65 -20
  329. package/skills/ai-delivery-orchestrate/references/workflow-model.md +7 -5
  330. package/skills/ai-delivery-orchestrate-release/SKILL.md +2 -2
  331. package/skills/ai-delivery-orchestrate-release/agents/openai.yaml +4 -4
  332. package/skills/ai-delivery-orchestrate-release/references/release-workflow-contract.md +1 -1
  333. package/skills/ai-delivery-plan-iteration/SKILL.md +11 -4
  334. package/skills/ai-delivery-plan-iteration/agents/openai.yaml +4 -4
  335. package/skills/ai-delivery-plan-iteration/assets/planning-template/scope-manifest.yaml +17 -0
  336. package/skills/ai-delivery-plan-iteration/assets/planning-template/slice-prototype-traceability.csv +1 -1
  337. package/skills/ai-delivery-plan-iteration/references/planning-contract.md +8 -2
  338. package/skills/ai-delivery-prepare-platform/SKILL.md +5 -1
  339. package/skills/ai-delivery-prepare-platform/agents/openai.yaml +4 -4
  340. package/skills/ai-delivery-prepare-release/agents/openai.yaml +4 -4
  341. package/skills/ai-delivery-review-change/SKILL.md +14 -4
  342. package/skills/ai-delivery-review-change/agents/openai.yaml +4 -4
  343. package/skills/ai-delivery-review-change/references/review-contract.md +6 -0
  344. package/skills/ai-delivery-triage-failure/SKILL.md +5 -1
  345. package/skills/ai-delivery-triage-failure/agents/openai.yaml +4 -4
  346. package/skills/ai-delivery-validate-artifacts/SKILL.md +6 -3
  347. package/skills/ai-delivery-validate-artifacts/agents/openai.yaml +4 -4
  348. package/skills/ai-delivery-validate-artifacts/references/artifact-contract.md +13 -13
  349. package/skills/ai-delivery-verify-candidate/SKILL.md +13 -6
  350. package/skills/ai-delivery-verify-candidate/agents/openai.yaml +3 -3
  351. package/skills/ai-delivery-verify-candidate/assets/verification-template/ui-acceptance-request.yaml +19 -0
  352. package/skills/ai-delivery-verify-candidate/assets/verification-template/ui-acceptance-results.yaml +53 -0
  353. package/skills/ai-delivery-verify-candidate/references/candidate-verification-contract.md +10 -2
  354. package/skills/ai-delivery-verify-production/agents/openai.yaml +3 -3
  355. package/skills/ai-delivery-design-experience/assets/experience-template/low-fidelity-confirmation.yaml +0 -32
  356. package/skills/ai-delivery-orchestrate/assets/project-template/.workflow/tools/viewer/public/app.js +0 -546
  357. package/skills/ai-delivery-orchestrate/assets/project-template/.workflow/tools/viewer/public/index.html +0 -199
  358. package/skills/ai-delivery-orchestrate/assets/project-template/.workflow/tools/viewer/public/styles.css +0 -529
  359. package/skills/ai-delivery-orchestrate/assets/project-template/.workflow/tools/viewer/server.mjs +0 -780
  360. package/skills/ai-delivery-orchestrate/assets/project-template/.workflow/tools/viewer/start.cmd +0 -4
  361. package/skills/ai-delivery-orchestrate/assets/project-template/.workflow/tools/viewer/start.ps1 +0 -2
@@ -0,0 +1,1847 @@
1
+ import crypto from "node:crypto";
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+ import { pathToFileURL, fileURLToPath } from "node:url";
5
+
6
+ import { buildPrototypeReviewTarget } from "./prototype-review-target.mjs";
7
+ import {
8
+ COMPONENT_CANDIDATE_BINDING_STATUS,
9
+ COMPONENT_CANDIDATE_LIFECYCLE_TRANSITIONS,
10
+ parsePrototypeCatalogCsv,
11
+ prototypeRevisionCatalogContracts,
12
+ serializePrototypeCatalogCsv,
13
+ validatePrototypeRevision,
14
+ } from "./prototype-revision.mjs";
15
+ import {
16
+ createPrototypePageReviewReceipt,
17
+ prototypePageReviewReceiptWrite,
18
+ } from "./prototype-page-review.mjs";
19
+ import {
20
+ commitWorkflowManagerAction,
21
+ preflightWorkflowManagerAction,
22
+ prepareWorkflowManagerFormalTransaction,
23
+ } from "./workflow-manager-transaction.mjs";
24
+ import { parse as parseYaml, stringify as stringifyYaml } from "./yaml-runtime.mjs";
25
+
26
+ const runtimeRoot = path.dirname(fileURLToPath(import.meta.url));
27
+ const installedStateRuntime = path.join(runtimeRoot, "..", "state", "delivery-state.mjs");
28
+ const packageStateRuntime = path.join(runtimeRoot, "delivery-state.mjs");
29
+ const { executeDeliveryStateCommand } = await import(pathToFileURL(
30
+ fs.existsSync(installedStateRuntime) ? installedStateRuntime : packageStateRuntime,
31
+ ).href);
32
+ const yaml = { parse: parseYaml, stringify: (value) => stringifyYaml(value, { lineWidth: 0 }) };
33
+
34
+ export const WORKFLOW_MANAGER_PROTOTYPES_SCHEMA_VERSION = "workflow-manager.prototypes.v2";
35
+ export const WORKFLOW_MANAGER_PROTOTYPE_SOURCE_SNAPSHOT_SCHEMA_VERSION =
36
+ "workflow-manager.prototype-source-snapshot.v1";
37
+
38
+ const REVIEW_ROOT = ".workflow/experience/reviews";
39
+ const REQUEST_ROOT = `${REVIEW_ROOT}/change-requests`;
40
+ const BATCH_ROOT = `${REVIEW_ROOT}/batches`;
41
+ const VISUAL_STANDARD_REVIEW_ROOT = `${REVIEW_ROOT}/visual-standard-reviews`;
42
+ const ATTACHMENT_ROOT = `${REVIEW_ROOT}/attachments`;
43
+ const TOMBSTONE_ROOT = `${REVIEW_ROOT}/discarded`;
44
+ const WRITE_LOCK = `${REVIEW_ROOT}/.prototype-write.lock`;
45
+ const SAFE_ID = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
46
+ const SHA256 = /^[a-f0-9]{64}$/;
47
+
48
+ export class PrototypeTerminalScopeError extends Error {
49
+ constructor(message) {
50
+ super(message);
51
+ this.name = "PrototypeTerminalScopeError";
52
+ this.code = "prototype-terminal-invalid";
53
+ }
54
+ }
55
+
56
+ export class PrototypeSourceSnapshotError extends Error {
57
+ constructor(code, message) {
58
+ super(message);
59
+ this.name = "PrototypeSourceSnapshotError";
60
+ this.code = code;
61
+ }
62
+ }
63
+
64
+ function digest(value) {
65
+ return crypto.createHash("sha256").update(value).digest("hex");
66
+ }
67
+
68
+ function inside(root, candidate) {
69
+ const relative = path.relative(root, candidate);
70
+ return relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative));
71
+ }
72
+
73
+ function contained(root, relative, label) {
74
+ if (typeof relative !== "string" || !relative || relative.includes("\0") || relative.includes("\\")
75
+ || path.posix.isAbsolute(relative) || path.posix.normalize(relative) !== relative) {
76
+ throw new Error(`${label} must be a normalized project-relative POSIX path`);
77
+ }
78
+ const result = path.resolve(root, ...relative.split("/"));
79
+ if (!inside(root, result)) throw new Error(`${label} escapes project root`);
80
+ return result;
81
+ }
82
+
83
+ function readJson(file, label) {
84
+ try {
85
+ const value = JSON.parse(fs.readFileSync(file, "utf8"));
86
+ if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("mapping required");
87
+ return value;
88
+ } catch (error) {
89
+ throw new Error(`${label} is invalid JSON: ${error.message}`);
90
+ }
91
+ }
92
+
93
+ function files(root, relative) {
94
+ const directory = contained(root, relative, `${relative} directory`);
95
+ if (!fs.existsSync(directory)) return [];
96
+ if (!fs.statSync(directory).isDirectory()) throw new Error(`${relative} must be a directory`);
97
+ return fs.readdirSync(directory, { withFileTypes: true }).filter((entry) => {
98
+ if (entry.isSymbolicLink()) throw new Error(`${relative} must not contain symbolic links`);
99
+ return entry.isFile() && entry.name.endsWith(".json");
100
+ }).map((entry) => path.join(directory, entry.name)).sort();
101
+ }
102
+
103
+ function requestFile(root, changeRequestId) {
104
+ if (!SAFE_ID.test(String(changeRequestId || ""))) throw new Error("Prototype Change Request identity is invalid");
105
+ return contained(root, `${REQUEST_ROOT}/${changeRequestId}.json`, "Prototype Change Request path");
106
+ }
107
+
108
+ function readRequest(root, changeRequestId) {
109
+ const file = requestFile(root, changeRequestId);
110
+ if (!fs.existsSync(file)) throw new Error(`Prototype Change Request does not exist: ${changeRequestId}`);
111
+ const request = readJson(file, "Prototype Change Request");
112
+ if (request.change_request_id !== changeRequestId || !Number.isInteger(request.revision)) {
113
+ throw new Error("Prototype Change Request identity or revision is invalid");
114
+ }
115
+ return { file, request };
116
+ }
117
+
118
+ function preflightAction(root, action, input, subject = null) {
119
+ return preflightWorkflowManagerAction(root, { action, input, subject });
120
+ }
121
+
122
+ function commitAction(root, action, input, subject, result, writes, timestamp, options = {}) {
123
+ const terminalId = result?.terminal_id
124
+ ?? result?.change_request?.terminal_id
125
+ ?? result?.page_review_receipt?.terminal_id
126
+ ?? input.terminal_id
127
+ ?? null;
128
+ return commitWorkflowManagerAction(root, {
129
+ action,
130
+ input,
131
+ subject,
132
+ result,
133
+ writes,
134
+ receiptPrefix: "WMPR",
135
+ timestamp,
136
+ transactionHooks: options.transactionHooks,
137
+ detail: {
138
+ terminal_id: terminalId,
139
+ result_revision: result?.revision ?? result?.change_request?.revision ?? null,
140
+ result_receipt_id: result?.receipt_id ?? result?.page_review_receipt?.receipt_id ?? null,
141
+ result_decision: result?.decision ?? result?.page_review_receipt?.decision ?? null,
142
+ ...(Array.isArray(result?.affected_identities)
143
+ ? { affected_identities: result.affected_identities } : {}),
144
+ },
145
+ eventDetail: { terminal_id: terminalId },
146
+ });
147
+ }
148
+
149
+ function withPrototypeWriteLock(root, action) {
150
+ const lockFile = contained(root, WRITE_LOCK, "Prototype write lock");
151
+ fs.mkdirSync(path.dirname(lockFile), { recursive: true });
152
+ let handle;
153
+ try {
154
+ handle = fs.openSync(lockFile, "wx");
155
+ fs.writeFileSync(handle, `${JSON.stringify({ action, pid: process.pid, acquired_at: new Date().toISOString() })}\n`);
156
+ } catch (error) {
157
+ if (error.code === "EEXIST") throw new Error("Another Prototype write is in progress; retry with the same idempotency key");
158
+ throw error;
159
+ }
160
+ return () => {
161
+ try { fs.closeSync(handle); } finally { fs.rmSync(lockFile, { force: true }); }
162
+ };
163
+ }
164
+
165
+ function guardedPrototypeWrite(root, action, input, subject, apply) {
166
+ const initialReplay = preflightAction(root, action, input, subject);
167
+ if (initialReplay) return { replay: initialReplay };
168
+ const release = withPrototypeWriteLock(root, action);
169
+ try {
170
+ const replay = preflightAction(root, action, input, subject);
171
+ return replay ? { replay } : { value: apply() };
172
+ } finally {
173
+ release();
174
+ }
175
+ }
176
+
177
+ function requireHuman(actor) {
178
+ if (!/^human:[A-Za-z0-9._@/-]+$/.test(String(actor || ""))) {
179
+ throw new Error("Prototype review write requires actor human:<identity>");
180
+ }
181
+ }
182
+
183
+ function requestSnapshot(request) {
184
+ return {
185
+ change_request_id: request.change_request_id,
186
+ revision: request.revision,
187
+ checksum: request.checksum,
188
+ };
189
+ }
190
+
191
+ function requestChecksum(request) {
192
+ const copy = structuredClone(request);
193
+ delete copy.checksum;
194
+ return digest(JSON.stringify(copy));
195
+ }
196
+
197
+ function formalInspection(root) {
198
+ return executeDeliveryStateCommand(["inspect", "--project", root], { yaml });
199
+ }
200
+
201
+ function approvedPrototypeGate(inspection, revisionId, revisionChecksum) {
202
+ return [...(inspection.workflow_state?.development_state?.gate_decisions || [])].reverse()
203
+ .find((decision) => decision.gate_id === "UX-UI"
204
+ && decision.decision === "approved"
205
+ && decision.prototype_revision_id === revisionId
206
+ && decision.prototype_revision_checksum === revisionChecksum);
207
+ }
208
+
209
+ function assertPrototypeRevisionWritable(root, revisionId, revisionChecksum) {
210
+ if (approvedPrototypeGate(formalInspection(root), revisionId, revisionChecksum)) {
211
+ throw new Error("Prototype revision is already approved by UX-UI; create a fresh Prototype revision before further review changes");
212
+ }
213
+ }
214
+
215
+ function requireTerminalIdentity(value) {
216
+ if (typeof value !== "string" || !SAFE_ID.test(value)) {
217
+ throw new PrototypeTerminalScopeError("Logical Terminal identity is required.");
218
+ }
219
+ return value;
220
+ }
221
+
222
+ function resolveTerminalScopedReviewTarget(target, input) {
223
+ const terminalId = requireTerminalIdentity(input.terminal_id);
224
+ const terminal = target.terminals.find((item) => item.terminal_id === terminalId);
225
+ if (!terminal) throw new PrototypeTerminalScopeError(`Logical Terminal is not registered: ${terminalId}`);
226
+ const page = target.pages.find((item) => item.page_id === input.page_id);
227
+ if (!page) throw new Error(`Prototype Page does not exist: ${input.page_id}`);
228
+ if (page.terminal_id !== terminalId) {
229
+ throw new PrototypeTerminalScopeError(`Prototype Page ${page.page_id} does not belong to Logical Terminal ${terminalId}.`);
230
+ }
231
+ const sceneId = input.review_scene_id;
232
+ if (sceneId === undefined || sceneId === null || sceneId === "") return { terminal, page, scene: null };
233
+ const scene = target.reviewScenes.find((item) => item.review_scene_id === sceneId);
234
+ if (!scene) throw new Error(`Review Scene does not exist: ${sceneId}`);
235
+ if (scene.page_id !== page.page_id || scene.terminal_id !== terminalId) {
236
+ throw new PrototypeTerminalScopeError(`Review Scene ${scene.review_scene_id} is outside Page ${page.page_id} or Logical Terminal ${terminalId}.`);
237
+ }
238
+ return { terminal, page, scene };
239
+ }
240
+
241
+ function targetForPrototypeRequest(root, input) {
242
+ const target = buildPrototypeReviewTarget(root, {
243
+ experience: input.experience || ".workflow/experience",
244
+ revision: input.revision,
245
+ });
246
+ if (target.mode !== "prototype-revision") throw new Error("Prototype Change Request requires an exact Prototype revision");
247
+ if (target.targetId !== input.prototype_revision_id || target.targetChecksum !== input.prototype_revision_checksum) {
248
+ throw new Error("Prototype Change Request revision or checksum is stale.");
249
+ }
250
+ const resolved = resolveTerminalScopedReviewTarget(target, input);
251
+ if (resolved.page.page_checksum !== input.page_checksum) {
252
+ throw new Error("Prototype Change Request Page checksum is stale.");
253
+ }
254
+ return { target, ...resolved };
255
+ }
256
+
257
+ function targetForVisualStandardReview(root, input) {
258
+ const target = buildPrototypeReviewTarget(root, {
259
+ experience: input.experience || ".workflow/experience",
260
+ revision: input.revision,
261
+ });
262
+ if (target.mode !== "prototype-revision") throw new Error("Visual Standard review requires an exact Prototype revision");
263
+ if (target.targetId !== input.prototype_revision_id || target.targetChecksum !== input.prototype_revision_checksum) {
264
+ throw new Error("Visual Standard review revision or checksum is stale.");
265
+ }
266
+ if (target.visualStandard.checksum !== input.visual_standard_checksum) {
267
+ throw new Error("Visual Standard review checksum is stale.");
268
+ }
269
+ const terminalId = input.terminal_id === "all" ? "all" : requireTerminalIdentity(input.terminal_id);
270
+ if (terminalId !== "all" && !target.terminals.some((terminal) => terminal.terminal_id === terminalId)) {
271
+ throw new PrototypeTerminalScopeError(`Logical Terminal is not registered: ${terminalId}`);
272
+ }
273
+ return { target, terminalId };
274
+ }
275
+
276
+ const COMPONENT_CANDIDATE_TRANSITION_SCHEMA_VERSION = "workflow-manager.component-candidate-transition.v1";
277
+
278
+ function readPrototypeRevisionDescriptor(file) {
279
+ const revision = yaml.parse(fs.readFileSync(file, "utf8"));
280
+ if (!revision || typeof revision !== "object" || Array.isArray(revision)) {
281
+ throw new Error("Prototype revision descriptor is invalid.");
282
+ }
283
+ return revision;
284
+ }
285
+
286
+ function bundleFileContents(bundleRoot) {
287
+ const contents = new Map();
288
+ const visit = (current) => {
289
+ for (const entry of fs.readdirSync(current, { withFileTypes: true })) {
290
+ const absolute = path.join(current, entry.name);
291
+ if (entry.isDirectory()) visit(absolute);
292
+ else if (entry.isFile()) {
293
+ const relative = path.relative(bundleRoot, absolute).replaceAll("\\", "/");
294
+ contents.set(relative, fs.readFileSync(absolute));
295
+ } else if (entry.isSymbolicLink()) {
296
+ throw new Error(`Prototype review bundle must not contain symbolic links: ${absolute}`);
297
+ } else throw new Error(`Prototype review bundle contains an unsupported filesystem entry: ${absolute}`);
298
+ }
299
+ };
300
+ visit(bundleRoot);
301
+ return contents;
302
+ }
303
+
304
+ function bundleDigest(contents) {
305
+ const hash = crypto.createHash("sha256");
306
+ for (const relative of [...contents.keys()].sort((left, right) => left.localeCompare(right))) {
307
+ hash.update(relative);
308
+ hash.update("\0");
309
+ hash.update(contents.get(relative));
310
+ hash.update("\0");
311
+ }
312
+ return hash.digest("hex");
313
+ }
314
+
315
+ function csvCatalog(contents, revision, name) {
316
+ const entry = revision.catalogs?.[name];
317
+ if (!entry || entry.algorithm !== "sha256" || typeof entry.ref !== "string") {
318
+ throw new Error(`Prototype ${name} catalog is invalid.`);
319
+ }
320
+ const bytes = contents.get(entry.ref);
321
+ if (!bytes) throw new Error(`Prototype ${name} catalog is missing from the review bundle.`);
322
+ const parsed = parsePrototypeCatalogCsv(bytes.toString("utf8"), `Prototype ${name} catalog`);
323
+ if (parsed.header !== prototypeRevisionCatalogContracts[name]) {
324
+ throw new Error(`Prototype ${name} catalog header is invalid.`);
325
+ }
326
+ return { entry, headers: parsed.header.split(","), rows: parsed.rows };
327
+ }
328
+
329
+ function transitionCandidateTarget(root, input) {
330
+ const target = buildPrototypeReviewTarget(root, {
331
+ experience: input.experience || ".workflow/experience",
332
+ revision: input.revision,
333
+ });
334
+ if (target.mode !== "prototype-revision") {
335
+ throw new Error("Component Candidate transition requires an exact Prototype revision.");
336
+ }
337
+ if (target.targetId !== input.prototype_revision_id || target.targetChecksum !== input.prototype_revision_checksum) {
338
+ throw new Error("Component Candidate transition revision or checksum is stale.");
339
+ }
340
+ assertPrototypeRevisionWritable(root, target.targetId, target.targetChecksum);
341
+ if (!SAFE_ID.test(String(input.component_id || ""))) {
342
+ throw new Error("Component Candidate identity is invalid.");
343
+ }
344
+ const candidate = target.componentGovernance.component_candidates
345
+ .find((item) => item.component_id === input.component_id);
346
+ if (!candidate) throw new Error(`Component Candidate does not exist: ${input.component_id}`);
347
+ const terminalId = requireTerminalIdentity(input.terminal_id);
348
+ if (candidate.source_terminal_id !== terminalId) {
349
+ throw new PrototypeTerminalScopeError(
350
+ `Component Candidate ${candidate.component_id} does not belong to Logical Terminal ${terminalId}.`,
351
+ );
352
+ }
353
+ if (candidate.lifecycle !== input.expected_lifecycle) {
354
+ throw new Error(
355
+ `Component Candidate ${candidate.component_id} lifecycle is ${candidate.lifecycle}; expected ${input.expected_lifecycle}.`,
356
+ );
357
+ }
358
+ const allowed = COMPONENT_CANDIDATE_LIFECYCLE_TRANSITIONS[candidate.lifecycle] || [];
359
+ if (!allowed.includes(input.target_lifecycle)) {
360
+ throw new Error(
361
+ `Component Candidate ${candidate.component_id} cannot transition from ${candidate.lifecycle} to ${input.target_lifecycle}.`,
362
+ );
363
+ }
364
+ const revision = readPrototypeRevisionDescriptor(target.revisionFile);
365
+ if (revision.status !== "frozen" || revision.artifact_id !== target.targetId || revision.version < 1) {
366
+ throw new Error("Component Candidate transition input must be a frozen Prototype revision.");
367
+ }
368
+ return { target, candidate, terminalId, revision };
369
+ }
370
+
371
+ function nextPrototypeRevisionIdentity(revision) {
372
+ const version = Number(revision.version) + 1;
373
+ if (!Number.isInteger(version) || version < 2) {
374
+ throw new Error("Prototype revision version is invalid for a successor draft.");
375
+ }
376
+ return {
377
+ artifact_id: `PROTOTYPE-REVISION-${String(version).padStart(4, "0")}`,
378
+ version,
379
+ };
380
+ }
381
+
382
+ function requireTransitionIdempotencyKey(value) {
383
+ if (typeof value !== "string" || !value.trim()) {
384
+ throw new Error("Component Candidate transition requires a non-empty idempotency key.");
385
+ }
386
+ }
387
+
388
+ function buildComponentCandidateTransition(root, input) {
389
+ const { target, candidate, terminalId, revision } = transitionCandidateTarget(root, input);
390
+ const successor = nextPrototypeRevisionIdentity(revision);
391
+ const successorRoot = path.join(path.dirname(path.dirname(target.revisionFile)), successor.artifact_id);
392
+ const successorRevisionFile = path.join(successorRoot, "prototype-revision.yaml");
393
+ if (fs.existsSync(successorRoot)) {
394
+ throw new Error(`Successor Prototype draft already exists: ${successor.artifact_id}`);
395
+ }
396
+
397
+ const contents = bundleFileContents(target.contentRoot);
398
+ const componentCatalog = csvCatalog(contents, revision, "components");
399
+ const candidateCatalog = csvCatalog(contents, revision, "component_candidates");
400
+ const bindingCatalog = csvCatalog(contents, revision, "component_bindings");
401
+ const implementationCatalog = csvCatalog(contents, revision, "implementation_bindings");
402
+ const candidateRow = candidateCatalog.rows.find((item) => item.component_id === candidate.component_id);
403
+ if (!candidateRow) throw new Error(`Component Candidate catalog entry is missing: ${candidate.component_id}`);
404
+ candidateRow.lifecycle = input.target_lifecycle;
405
+ contents.set(
406
+ candidateCatalog.entry.ref,
407
+ Buffer.from(serializePrototypeCatalogCsv(candidateCatalog.headers, candidateCatalog.rows)),
408
+ );
409
+
410
+ const requiredBindingStatus = COMPONENT_CANDIDATE_BINDING_STATUS[input.target_lifecycle];
411
+ const affectedBindings = bindingCatalog.rows.filter((binding) => binding.component_id === candidate.component_id);
412
+ for (const binding of affectedBindings) binding.status = requiredBindingStatus;
413
+ if (affectedBindings.length) {
414
+ contents.set(
415
+ bindingCatalog.entry.ref,
416
+ Buffer.from(serializePrototypeCatalogCsv(bindingCatalog.headers, bindingCatalog.rows)),
417
+ );
418
+ }
419
+
420
+ const retainedComponents = input.target_lifecycle === "registered"
421
+ ? componentCatalog.rows
422
+ : componentCatalog.rows.filter((component) => component.component_id !== candidate.component_id);
423
+ const removedComponentRegistrations = componentCatalog.rows.length - retainedComponents.length;
424
+ if (removedComponentRegistrations) {
425
+ contents.set(
426
+ componentCatalog.entry.ref,
427
+ Buffer.from(serializePrototypeCatalogCsv(componentCatalog.headers, retainedComponents)),
428
+ );
429
+ }
430
+
431
+ const retainedImplementationBindings = input.target_lifecycle === "registered"
432
+ ? implementationCatalog.rows
433
+ : implementationCatalog.rows.filter((binding) => binding.component_id !== candidate.component_id);
434
+ const removedImplementationMappings = implementationCatalog.rows.length - retainedImplementationBindings.length;
435
+ if (removedImplementationMappings) {
436
+ contents.set(
437
+ implementationCatalog.entry.ref,
438
+ Buffer.from(serializePrototypeCatalogCsv(implementationCatalog.headers, retainedImplementationBindings)),
439
+ );
440
+ }
441
+
442
+ const draftRevision = structuredClone(revision);
443
+ draftRevision.artifact_id = successor.artifact_id;
444
+ draftRevision.version = successor.version;
445
+ draftRevision.status = "draft";
446
+ draftRevision.frozen_at = null;
447
+ draftRevision.previous_revision = {
448
+ ref: path.relative(successorRoot, target.revisionFile).replaceAll("\\", "/"),
449
+ algorithm: "sha256",
450
+ checksum: target.targetChecksum,
451
+ };
452
+ for (const [name, entry] of Object.entries(draftRevision.catalogs || {})) {
453
+ const bytes = contents.get(entry.ref);
454
+ if (!bytes) throw new Error(`Successor Prototype ${name} catalog is missing.`);
455
+ entry.checksum = digest(bytes);
456
+ }
457
+ draftRevision.review_bundle.checksum = bundleDigest(contents);
458
+ const revisionContent = yaml.stringify(draftRevision);
459
+ const successorChecksum = digest(revisionContent);
460
+ const successorRevisionRef = path.relative(target.experienceRoot, successorRevisionFile).replaceAll("\\", "/");
461
+ const catalogChanges = [
462
+ {
463
+ catalog: "component_candidates",
464
+ operation: "lifecycle-transition",
465
+ affected_count: 1,
466
+ from_lifecycle: candidate.lifecycle,
467
+ to_lifecycle: input.target_lifecycle,
468
+ },
469
+ {
470
+ catalog: "component_bindings",
471
+ operation: "binding-status-update",
472
+ affected_count: affectedBindings.length,
473
+ status: requiredBindingStatus,
474
+ },
475
+ {
476
+ catalog: "components",
477
+ operation: "remove-superseded-registration",
478
+ affected_count: removedComponentRegistrations,
479
+ },
480
+ {
481
+ catalog: "implementation_bindings",
482
+ operation: "remove-ineligible-mappings",
483
+ affected_count: removedImplementationMappings,
484
+ },
485
+ ];
486
+ const planBase = {
487
+ schema_version: COMPONENT_CANDIDATE_TRANSITION_SCHEMA_VERSION,
488
+ action: "prototype-component-candidate-transition-plan",
489
+ outcome: "planned",
490
+ prototype_revision: {
491
+ revision_ref: target.revisionRelative,
492
+ artifact_id: target.targetId,
493
+ checksum: target.targetChecksum,
494
+ version: revision.version,
495
+ },
496
+ component_candidate: {
497
+ component_id: candidate.component_id,
498
+ terminal_id: terminalId,
499
+ source_page_id: candidate.source_page_id,
500
+ source_scene_ids: candidate.source_scene_ids,
501
+ data_contract_id: candidate.data_contract_id,
502
+ from_lifecycle: candidate.lifecycle,
503
+ to_lifecycle: input.target_lifecycle,
504
+ },
505
+ successor_revision: {
506
+ artifact_id: successor.artifact_id,
507
+ revision_ref: successorRevisionRef,
508
+ checksum: successorChecksum,
509
+ version: successor.version,
510
+ status: "draft",
511
+ previous_revision: draftRevision.previous_revision,
512
+ },
513
+ catalog_changes: catalogChanges,
514
+ affected_identities: [
515
+ target.targetId,
516
+ candidate.component_id,
517
+ ...affectedBindings.map((binding) => `${binding.page_id}:${binding.scene_ids}`),
518
+ ],
519
+ };
520
+ const plan = { ...planBase, plan_checksum: digest(JSON.stringify(planBase)) };
521
+ const writes = new Map();
522
+ const successorBundleRoot = path.join(
523
+ successorRoot,
524
+ ...String(draftRevision.review_bundle.ref).split("/"),
525
+ );
526
+ for (const [relative, content] of contents) {
527
+ writes.set(path.join(successorBundleRoot, ...relative.split("/")), content);
528
+ }
529
+ writes.set(successorRevisionFile, revisionContent);
530
+ return {
531
+ plan,
532
+ writes: [...writes].map(([file, content]) => ({ file, content })),
533
+ };
534
+ }
535
+
536
+ export function planWorkflowManagerComponentCandidateTransition(projectRoot, input = {}) {
537
+ const root = path.resolve(projectRoot);
538
+ requireHuman(input.actor);
539
+ requireTransitionIdempotencyKey(input.idempotency_key);
540
+ return buildComponentCandidateTransition(root, input).plan;
541
+ }
542
+
543
+ export function applyWorkflowManagerComponentCandidateTransition(projectRoot, input = {}, options = {}) {
544
+ const root = path.resolve(projectRoot);
545
+ requireHuman(input.actor);
546
+ requireTransitionIdempotencyKey(input.idempotency_key);
547
+ const guarded = guardedPrototypeWrite(
548
+ root,
549
+ "prototype-component-candidate-transition-apply",
550
+ input,
551
+ input.component_id,
552
+ () => {
553
+ const prepared = buildComponentCandidateTransition(root, input);
554
+ if (input.expected_plan_checksum !== prepared.plan.plan_checksum) {
555
+ throw new Error("Component Candidate transition plan checksum is stale.");
556
+ }
557
+ const result = {
558
+ schema_version: COMPONENT_CANDIDATE_TRANSITION_SCHEMA_VERSION,
559
+ component_id: input.component_id,
560
+ terminal_id: input.terminal_id,
561
+ from_lifecycle: input.expected_lifecycle,
562
+ to_lifecycle: input.target_lifecycle,
563
+ input_revision: prepared.plan.prototype_revision,
564
+ successor_revision: prepared.plan.successor_revision,
565
+ catalog_changes: prepared.plan.catalog_changes,
566
+ affected_identities: prepared.plan.affected_identities,
567
+ plan_checksum: prepared.plan.plan_checksum,
568
+ };
569
+ const recorded = commitAction(
570
+ root,
571
+ "prototype-component-candidate-transition-apply",
572
+ input,
573
+ input.component_id,
574
+ result,
575
+ prepared.writes,
576
+ new Date().toISOString(),
577
+ options,
578
+ );
579
+ return { outcome: "applied", ...result, receipt: recorded.receipt, event: recorded.event };
580
+ },
581
+ );
582
+ if (guarded.replay) {
583
+ return { outcome: "idempotent-replay", ...guarded.replay.result, receipt: guarded.replay };
584
+ }
585
+ return guarded.value;
586
+ }
587
+
588
+ function assertStoredPrototypeRequestTerminal(root, request) {
589
+ return targetForPrototypeRequest(root, {
590
+ prototype_revision_id: request.prototype_revision_id,
591
+ prototype_revision_checksum: request.prototype_revision_checksum,
592
+ terminal_id: request.terminal_id,
593
+ page_id: request.page_id,
594
+ page_checksum: request.page_checksum,
595
+ review_scene_id: request.review_scene_id,
596
+ });
597
+ }
598
+
599
+ function currentRequests(root) {
600
+ return files(root, REQUEST_ROOT)
601
+ .map((file) => readJson(file, "Prototype Change Request"))
602
+ .filter((request) => request.status !== "discarded");
603
+ }
604
+
605
+ function batches(root) {
606
+ return files(root, BATCH_ROOT).map((file) => readJson(file, "Prototype Review Batch"));
607
+ }
608
+
609
+ function tombstones(root) {
610
+ return files(root, TOMBSTONE_ROOT).map((file) => readJson(file, "Prototype Change Request tombstone"));
611
+ }
612
+
613
+ function visualStandardReviewFile(root, reviewId) {
614
+ if (!SAFE_ID.test(String(reviewId || ""))) throw new Error("Visual Standard review identity is invalid");
615
+ return contained(root, `${VISUAL_STANDARD_REVIEW_ROOT}/${reviewId}.json`, "Visual Standard review path");
616
+ }
617
+
618
+ function visualStandardReviews(root) {
619
+ return files(root, VISUAL_STANDARD_REVIEW_ROOT)
620
+ .map((file) => readJson(file, "Visual Standard review"))
621
+ .sort((left, right) => String(left.created_at || "").localeCompare(String(right.created_at || "")));
622
+ }
623
+
624
+ const REVISION_HISTORY_COLLECTIONS = [
625
+ ["terminals", "terminal_id"],
626
+ ["pages", "page_id"],
627
+ ["review_scenes", "review_scene_id"],
628
+ ["trigger_conditions", "trigger_condition_id"],
629
+ ["routes", "route_id"],
630
+ ["ui_mapping", "ui_mapping_id"],
631
+ ["review_units", "review_unit_id"],
632
+ ["review_unit_pages", (row) => [row.review_unit_id, row.page_id].join(":")],
633
+ ["component_bindings", (row) => [
634
+ row.page_id,
635
+ row.scene_ids,
636
+ row.component_id,
637
+ row.variant,
638
+ row.data_contract_id,
639
+ ].join(":")],
640
+ ["component_candidates", "component_id"],
641
+ ["components", "component_id"],
642
+ ["data_contracts", "data_contract_id"],
643
+ ["implementation_bindings", "component_id"],
644
+ ["source_inspection_snapshots", "source_snapshot_id"],
645
+ ];
646
+
647
+ function containedFrom(root, relative, label) {
648
+ if (typeof relative !== "string" || !relative || relative.includes("\0") || relative.includes("\\")
649
+ || path.posix.isAbsolute(relative) || path.posix.normalize(relative) !== relative) {
650
+ throw new Error(`${label} must be a normalized relative POSIX path`);
651
+ }
652
+ const result = path.resolve(root, ...relative.split("/"));
653
+ if (!inside(root, result)) throw new Error(`${label} escapes its declared root`);
654
+ return result;
655
+ }
656
+
657
+ function containedWithoutSymlinks(root, relative, label) {
658
+ const result = containedFrom(root, relative, label);
659
+ const resolvedRoot = path.resolve(root);
660
+ if (!fs.existsSync(resolvedRoot) || !fs.lstatSync(resolvedRoot).isDirectory()
661
+ || fs.lstatSync(resolvedRoot).isSymbolicLink()) {
662
+ throw new Error(`${label} root is missing or unsafe.`);
663
+ }
664
+ const segments = path.relative(resolvedRoot, result).split(path.sep).filter(Boolean);
665
+ let current = resolvedRoot;
666
+ for (const segment of segments) {
667
+ current = path.join(current, segment);
668
+ if (!fs.existsSync(current)) throw new Error(`${label} is missing.`);
669
+ if (fs.lstatSync(current).isSymbolicLink()) {
670
+ throw new Error(`${label} must not traverse symbolic links.`);
671
+ }
672
+ }
673
+ const realRoot = fs.realpathSync(resolvedRoot);
674
+ const realResult = fs.realpathSync(result);
675
+ if (!inside(realRoot, realResult)) throw new Error(`${label} escapes its declared root through a symbolic link.`);
676
+ return result;
677
+ }
678
+
679
+ function canonicalDigest(value) {
680
+ const normalize = (item) => {
681
+ if (Array.isArray(item)) return item.map(normalize);
682
+ if (!item || typeof item !== "object") return item;
683
+ return Object.fromEntries(Object.keys(item).sort().map((key) => [key, normalize(item[key])]));
684
+ };
685
+ return digest(JSON.stringify(normalize(value)));
686
+ }
687
+
688
+ function historyBundleRoot(revisionFile, revision) {
689
+ const bundleRoot = containedWithoutSymlinks(
690
+ path.dirname(revisionFile),
691
+ revision.review_bundle?.ref,
692
+ "Prototype revision history bundle",
693
+ );
694
+ if (!fs.statSync(bundleRoot).isDirectory()) throw new Error("Prototype revision history bundle is missing or unsafe.");
695
+ const referencedFiles = [
696
+ [revision.review_shell?.entrypoint_in_bundle, "Review Shell entrypoint"],
697
+ [revision.visual_standard?.ref, "Visual Standard evidence"],
698
+ [revision.review_evidence?.ref, "Prototype review evidence"],
699
+ ...Object.entries(revision.catalogs || {}).map(([name, entry]) => [
700
+ entry?.ref,
701
+ `Prototype revision history ${name} catalog`,
702
+ ]),
703
+ ];
704
+ for (const [relative, label] of referencedFiles) containedWithoutSymlinks(bundleRoot, relative, label);
705
+ return bundleRoot;
706
+ }
707
+
708
+ function historyCatalogRows(projectRoot, experience, experienceRoot, revisionFile, revision) {
709
+ const revisionRelative = path.relative(experienceRoot, revisionFile).replaceAll("\\", "/");
710
+ historyBundleRoot(revisionFile, revision);
711
+ validatePrototypeRevision(projectRoot, { experience, revision: revisionRelative });
712
+ const bundleRoot = historyBundleRoot(revisionFile, revision);
713
+ const rows = {};
714
+ for (const [name, expectedHeader] of Object.entries(prototypeRevisionCatalogContracts)) {
715
+ const entry = revision.catalogs[name];
716
+ if (entry?.algorithm !== "sha256" || !SHA256.test(String(entry.checksum || ""))) {
717
+ throw new Error(`Prototype revision history ${name} catalog checksum is invalid.`);
718
+ }
719
+ const file = containedWithoutSymlinks(bundleRoot, entry.ref, `Prototype revision history ${name} catalog`);
720
+ if (!fs.statSync(file).isFile()) {
721
+ throw new Error(`Prototype revision history ${name} catalog is missing or unsafe.`);
722
+ }
723
+ const bytes = fs.readFileSync(file);
724
+ if (digest(bytes) !== entry.checksum) {
725
+ throw new Error(`Prototype revision history ${name} catalog checksum mismatch.`);
726
+ }
727
+ const parsed = parsePrototypeCatalogCsv(bytes.toString("utf8"), `Prototype revision history ${name} catalog`);
728
+ if (parsed.header !== expectedHeader) {
729
+ throw new Error(`Prototype revision history ${name} catalog header is invalid.`);
730
+ }
731
+ rows[name] = parsed.rows;
732
+ }
733
+ return { revision, rows };
734
+ }
735
+
736
+ function historyRecordMap(rows, collection, identity) {
737
+ const mapped = new Map();
738
+ for (const row of rows) {
739
+ const key = typeof identity === "function" ? identity(row) : row[identity];
740
+ if (!String(key || "").trim() || mapped.has(key)) {
741
+ throw new Error(`Prototype revision history ${collection} identity is invalid.`);
742
+ }
743
+ mapped.set(key, canonicalDigest(row));
744
+ }
745
+ return mapped;
746
+ }
747
+
748
+ function terminalHistoryFacts(rows, terminalId) {
749
+ const terminals = rows.terminals.filter((terminal) => terminal.lifecycle_status === "active"
750
+ && (terminalId === "all" || terminal.terminal_id === terminalId));
751
+ if (terminalId !== "all" && !terminals.length) return null;
752
+ const terminalIds = new Set(terminals.map((terminal) => terminal.terminal_id));
753
+ const pages = rows.pages.filter((row) => terminalIds.has(row.terminal_id));
754
+ const pageIds = new Set(pages.map((page) => page.page_id));
755
+ const reviewScenes = rows.review_scenes.filter((row) => terminalIds.has(row.terminal_id));
756
+ const reviewSceneIds = new Set(reviewScenes.map((scene) => scene.review_scene_id));
757
+ const triggerConditionIds = new Set(reviewScenes.map((scene) => scene.trigger_condition_id));
758
+ const reviewUnitPages = rows.review_unit_pages.filter((row) => pageIds.has(row.page_id));
759
+ const reviewUnitIds = new Set(reviewUnitPages.map((row) => row.review_unit_id));
760
+ const reviewUnits = rows.review_units.filter((row) => reviewUnitIds.has(row.review_unit_id)
761
+ || (row.unit_kind === "page" && pageIds.has(row.target_id))
762
+ || (row.unit_kind === "review-scene" && reviewSceneIds.has(row.target_id)));
763
+ for (const row of reviewUnits) reviewUnitIds.add(row.review_unit_id);
764
+ const pageById = new Map(rows.pages.map((page) => [page.page_id, page]));
765
+ const componentBindings = rows.component_bindings.filter((row) => terminalIds.has(row.terminal_id));
766
+ const componentIds = new Set(componentBindings.map((row) => row.component_id));
767
+ const dataContractIds = new Set(componentBindings.map((row) => row.data_contract_id));
768
+ const componentCandidates = rows.component_candidates.filter((row) => {
769
+ const sourcePage = pageById.get(row.source_page_id);
770
+ if (!sourcePage || !terminalIds.has(sourcePage.terminal_id)) return false;
771
+ componentIds.add(row.component_id);
772
+ dataContractIds.add(row.data_contract_id);
773
+ return true;
774
+ });
775
+ const collectionRows = {
776
+ terminals,
777
+ pages,
778
+ review_scenes: reviewScenes,
779
+ trigger_conditions: rows.trigger_conditions.filter((row) => triggerConditionIds.has(row.trigger_condition_id)),
780
+ routes: rows.routes.filter((row) => terminalIds.has(row.terminal_id)),
781
+ ui_mapping: rows.ui_mapping.filter((row) => terminalIds.has(row.terminal_id)),
782
+ review_units: reviewUnits,
783
+ review_unit_pages: reviewUnitPages.filter((row) => reviewUnitIds.has(row.review_unit_id)),
784
+ component_bindings: componentBindings,
785
+ component_candidates: componentCandidates,
786
+ components: rows.components.filter((row) => componentIds.has(row.component_id)),
787
+ data_contracts: rows.data_contracts.filter((row) => dataContractIds.has(row.data_contract_id)),
788
+ implementation_bindings: rows.implementation_bindings.filter((row) => componentIds.has(row.component_id)),
789
+ source_inspection_snapshots: rows.source_inspection_snapshots.filter((row) => terminalIds.has(row.terminal_id)),
790
+ };
791
+ const collections = Object.fromEntries(REVISION_HISTORY_COLLECTIONS.map(([name, identity]) => [
792
+ name,
793
+ historyRecordMap(collectionRows[name], name, identity),
794
+ ]));
795
+ return {
796
+ terminal_ids: [...terminalIds].sort(),
797
+ collections,
798
+ catalog_summary: Object.fromEntries(Object.entries(collections).map(([name, value]) => [name, value.size])),
799
+ };
800
+ }
801
+
802
+ function unavailableRevisionHistoryEntry(metadata, descriptor, error) {
803
+ return {
804
+ ...metadata,
805
+ history_sort_version: Number.isInteger(descriptor?.version) ? descriptor.version : Number.MAX_SAFE_INTEGER,
806
+ inspection_status: "unavailable",
807
+ prototype_revision_id: null,
808
+ declared_prototype_revision_id: typeof descriptor?.artifact_id === "string" ? descriptor.artifact_id : null,
809
+ revision_status: "unavailable",
810
+ version: null,
811
+ iteration_id: null,
812
+ frozen_at: null,
813
+ inspection_error: {
814
+ code: "prototype-revision-invalid",
815
+ message: error.message,
816
+ },
817
+ terminal_facts: new Map(),
818
+ };
819
+ }
820
+
821
+ function readRevisionHistoryEntry(projectRoot, experience, experienceRoot, revisionFile) {
822
+ let metadata = {
823
+ path: path.relative(experienceRoot, revisionFile).replaceAll("\\", "/"),
824
+ checksum: null,
825
+ modified_at: null,
826
+ };
827
+ let descriptor = null;
828
+ try {
829
+ const bytes = fs.readFileSync(revisionFile);
830
+ metadata = {
831
+ ...metadata,
832
+ checksum: digest(bytes),
833
+ modified_at: fs.statSync(revisionFile).mtime.toISOString(),
834
+ };
835
+ descriptor = readPrototypeRevisionDescriptor(revisionFile);
836
+ if (descriptor.status === "draft") return null;
837
+ if (descriptor.status !== "frozen") throw new Error("Prototype revision history descriptor is not frozen.");
838
+ const { revision, rows } = historyCatalogRows(projectRoot, experience, experienceRoot, revisionFile, descriptor);
839
+ return {
840
+ ...metadata,
841
+ history_sort_version: revision.version,
842
+ inspection_status: "available",
843
+ prototype_revision_id: revision.artifact_id,
844
+ declared_prototype_revision_id: revision.artifact_id,
845
+ revision_status: revision.status || "unknown",
846
+ version: revision.version,
847
+ iteration_id: revision.iteration_id || null,
848
+ frozen_at: revision.frozen_at || null,
849
+ terminal_facts: new Map([
850
+ ["all", terminalHistoryFacts(rows, "all")],
851
+ ...rows.terminals.filter((terminal) => terminal.lifecycle_status === "active")
852
+ .map((terminal) => [terminal.terminal_id, terminalHistoryFacts(rows, terminal.terminal_id)]),
853
+ ]),
854
+ };
855
+ } catch (error) {
856
+ return unavailableRevisionHistoryEntry(metadata, descriptor, error);
857
+ }
858
+ }
859
+
860
+ function revisionHistory(projectRoot, experienceRoot, experience) {
861
+ const revisionsRoot = path.join(experienceRoot, "prototype", "revisions");
862
+ if (!fs.existsSync(revisionsRoot)) return [];
863
+ if (!fs.lstatSync(revisionsRoot).isDirectory() || fs.lstatSync(revisionsRoot).isSymbolicLink()) {
864
+ throw new Error("Prototype revision history root is missing or unsafe.");
865
+ }
866
+ const result = [];
867
+ const visit = (current) => {
868
+ for (const entry of fs.readdirSync(current, { withFileTypes: true })) {
869
+ const absolute = path.join(current, entry.name);
870
+ if (entry.isSymbolicLink()) {
871
+ if (entry.name === "prototype-revision.yaml") {
872
+ throw new Error("Prototype revision history descriptor must not be a symbolic link");
873
+ }
874
+ continue;
875
+ }
876
+ if (entry.isDirectory()) visit(absolute);
877
+ else if (entry.isFile() && entry.name === "prototype-revision.yaml") {
878
+ const historyEntry = readRevisionHistoryEntry(projectRoot, experience, experienceRoot, absolute);
879
+ if (historyEntry) result.push(historyEntry);
880
+ }
881
+ }
882
+ };
883
+ visit(revisionsRoot);
884
+ return result
885
+ .sort((left, right) => left.history_sort_version - right.history_sort_version || left.path.localeCompare(right.path))
886
+ .map(({ history_sort_version, ...entry }) => entry);
887
+ }
888
+
889
+ function revisionDiff(previous, current) {
890
+ const collections = Object.fromEntries(REVISION_HISTORY_COLLECTIONS.map(([name]) => {
891
+ const before = previous?.collections[name] || new Map();
892
+ const after = current.collections[name];
893
+ let added = 0;
894
+ let removed = 0;
895
+ let changed = 0;
896
+ for (const [identity, checksum] of after) {
897
+ if (!before.has(identity)) added += 1;
898
+ else if (before.get(identity) !== checksum) changed += 1;
899
+ }
900
+ for (const identity of before.keys()) {
901
+ if (!after.has(identity)) removed += 1;
902
+ }
903
+ return [name, { added, removed, changed }];
904
+ }));
905
+ return {
906
+ compared_to_revision_id: previous?.prototype_revision_id || null,
907
+ status: previous ? "compared" : "baseline",
908
+ changed_facts: Object.values(collections)
909
+ .reduce((total, collection) => total + collection.added + collection.removed + collection.changed, 0),
910
+ collections,
911
+ };
912
+ }
913
+
914
+ function scopedRevisionHistory(history, terminalId) {
915
+ const scoped = history.filter((entry) => entry.inspection_status === "unavailable" || entry.terminal_facts.has(terminalId));
916
+ let previousFacts = null;
917
+ let previousRevisionId = null;
918
+ let historyGap = false;
919
+ return scoped.map((entry) => {
920
+ if (entry.inspection_status === "unavailable") {
921
+ previousFacts = null;
922
+ previousRevisionId = null;
923
+ historyGap = true;
924
+ return {
925
+ path: entry.path,
926
+ checksum: entry.checksum,
927
+ modified_at: entry.modified_at,
928
+ inspection_status: entry.inspection_status,
929
+ prototype_revision_id: null,
930
+ declared_prototype_revision_id: entry.declared_prototype_revision_id,
931
+ revision_status: entry.revision_status,
932
+ version: null,
933
+ iteration_id: null,
934
+ frozen_at: null,
935
+ terminal_id: terminalId,
936
+ catalog_summary: null,
937
+ inspection_error: entry.inspection_error,
938
+ revision_diff: {
939
+ compared_to_revision_id: null,
940
+ status: "unavailable",
941
+ changed_facts: null,
942
+ collections: {},
943
+ },
944
+ };
945
+ }
946
+ const facts = entry.terminal_facts.get(terminalId);
947
+ const revisionDiffResult = historyGap
948
+ ? {
949
+ compared_to_revision_id: null,
950
+ status: "history-gap",
951
+ changed_facts: null,
952
+ collections: {},
953
+ }
954
+ : revisionDiff(
955
+ previousFacts ? { ...previousFacts, prototype_revision_id: previousRevisionId } : null,
956
+ facts,
957
+ );
958
+ const result = {
959
+ path: entry.path,
960
+ checksum: entry.checksum,
961
+ modified_at: entry.modified_at,
962
+ inspection_status: entry.inspection_status,
963
+ prototype_revision_id: entry.prototype_revision_id,
964
+ declared_prototype_revision_id: entry.declared_prototype_revision_id,
965
+ revision_status: entry.revision_status,
966
+ version: entry.version,
967
+ iteration_id: entry.iteration_id,
968
+ frozen_at: entry.frozen_at,
969
+ terminal_id: terminalId,
970
+ catalog_summary: facts.catalog_summary,
971
+ inspection_error: null,
972
+ revision_diff: revisionDiffResult,
973
+ };
974
+ previousFacts = facts;
975
+ previousRevisionId = entry.prototype_revision_id;
976
+ historyGap = false;
977
+ return result;
978
+ });
979
+ }
980
+
981
+ function emptyPrototypeProjection(root, experienceRoot, experience) {
982
+ return {
983
+ schema_version: WORKFLOW_MANAGER_PROTOTYPES_SCHEMA_VERSION,
984
+ registration_status: "not-registered",
985
+ prototype_revision_id: null,
986
+ prototype_revision_checksum: null,
987
+ catalog_schema_version: null,
988
+ terminals: [],
989
+ catalog_summary: {},
990
+ visual_standard: null,
991
+ component_governance: null,
992
+ pages: [],
993
+ review_scenes: [],
994
+ routes: [],
995
+ source_inspection_snapshots: [],
996
+ change_requests: currentRequests(root),
997
+ review_batches: batches(root),
998
+ discarded_tombstones: tombstones(root),
999
+ visual_standard_reviews: visualStandardReviews(root),
1000
+ revision_history: fs.existsSync(experienceRoot) ? revisionHistory(root, experienceRoot, experience) : [],
1001
+ terminal_scope: { terminal_id: "all", mode: "all" },
1002
+ };
1003
+ }
1004
+
1005
+ function requestedTerminalId(options) {
1006
+ const terminalId = options.terminal_id ?? options.terminalId ?? "all";
1007
+ if (terminalId === "all") return terminalId;
1008
+ if (typeof terminalId !== "string" || !SAFE_ID.test(terminalId)) {
1009
+ throw new PrototypeTerminalScopeError("Logical Terminal identity is invalid.");
1010
+ }
1011
+ return terminalId;
1012
+ }
1013
+
1014
+ function assertExpectedPrototypeRevisionChecksum(target, options) {
1015
+ const expected = options.expected_checksum ?? options.expectedChecksum;
1016
+ if (expected === undefined || expected === null || expected === "") return;
1017
+ if (!SHA256.test(String(expected))) throw new Error("Prototype revision checksum is invalid.");
1018
+ if (target.targetChecksum !== expected) throw new Error("Prototype revision checksum mismatch.");
1019
+ }
1020
+
1021
+ function componentGovernanceForTerminal(governance, terminalId) {
1022
+ if (!governance) return null;
1023
+ const componentBindings = governance.component_bindings.filter((binding) =>
1024
+ terminalId === "all" || binding.terminal_id === terminalId);
1025
+ const componentCandidates = (governance.component_candidates || []).filter((candidate) =>
1026
+ terminalId === "all"
1027
+ || candidate.source_terminal_id === terminalId
1028
+ || candidate.terminal_ids?.includes(terminalId));
1029
+ const componentIds = new Set(componentBindings.map((binding) => binding.component_id));
1030
+ const candidateIds = new Set(componentCandidates.map((candidate) => candidate.component_id));
1031
+ const dataContractIds = new Set([
1032
+ ...componentBindings.map((binding) => binding.data_contract_id),
1033
+ ...componentCandidates.map((candidate) => candidate.data_contract_id),
1034
+ ]);
1035
+ const components = governance.components
1036
+ .filter((component) => terminalId === "all" || componentIds.has(component.component_id))
1037
+ .map((component) => {
1038
+ const bindings = componentBindings.filter((binding) => binding.component_id === component.component_id);
1039
+ return {
1040
+ ...component,
1041
+ binding_count: bindings.length,
1042
+ terminal_ids: [...new Set(bindings.map((binding) => binding.terminal_id))].sort(),
1043
+ };
1044
+ });
1045
+ const dataContracts = governance.data_contracts
1046
+ .filter((contract) => terminalId === "all" || dataContractIds.has(contract.data_contract_id))
1047
+ .map((contract) => {
1048
+ const bindings = componentBindings.filter((binding) => binding.data_contract_id === contract.data_contract_id);
1049
+ return {
1050
+ ...contract,
1051
+ binding_count: bindings.length,
1052
+ terminal_ids: [...new Set(bindings.map((binding) => binding.terminal_id))].sort(),
1053
+ };
1054
+ });
1055
+ const implementationBindings = governance.implementation_bindings.filter((binding) =>
1056
+ terminalId === "all" || componentIds.has(binding.component_id) || candidateIds.has(binding.component_id));
1057
+ const counts = {
1058
+ "component-registrations": components.length,
1059
+ "component-candidates": componentCandidates.length,
1060
+ "data-contracts": dataContracts.length,
1061
+ "component-bindings": componentBindings.length,
1062
+ "implementation-mappings": implementationBindings.length,
1063
+ };
1064
+ return {
1065
+ ...governance,
1066
+ terminal_scope: { terminal_id: terminalId },
1067
+ components,
1068
+ component_candidates: componentCandidates,
1069
+ data_contracts: dataContracts,
1070
+ component_bindings: componentBindings,
1071
+ implementation_bindings: implementationBindings,
1072
+ validation: {
1073
+ ...governance.validation,
1074
+ checks: governance.validation.checks.map((check) => ({
1075
+ ...check,
1076
+ count: counts[check.check_id] ?? check.count,
1077
+ })),
1078
+ },
1079
+ };
1080
+ }
1081
+
1082
+ function visualStandardReviewsForTerminal(reviews, terminalId) {
1083
+ if (terminalId === "all") return reviews;
1084
+ return reviews.filter((review) => review.terminal_id === "all" || review.terminal_id === terminalId);
1085
+ }
1086
+
1087
+ function withTerminalScope(projection, options) {
1088
+ const terminalId = requestedTerminalId(options);
1089
+ if (terminalId === "all") {
1090
+ return {
1091
+ ...projection,
1092
+ revision_history: scopedRevisionHistory(projection.revision_history || [], terminalId),
1093
+ component_governance: componentGovernanceForTerminal(projection.component_governance, terminalId),
1094
+ visual_standard_reviews: visualStandardReviewsForTerminal(projection.visual_standard_reviews || [], terminalId),
1095
+ terminal_scope: { terminal_id: "all", mode: "all" },
1096
+ };
1097
+ }
1098
+ const terminal = projection.terminals.find((item) => item.terminal_id === terminalId);
1099
+ if (!terminal) {
1100
+ throw new PrototypeTerminalScopeError(`Logical Terminal is not registered: ${terminalId}`);
1101
+ }
1102
+ const pages = projection.pages.filter((page) => page.terminal_id === terminalId);
1103
+ const reviewScenes = projection.review_scenes.filter((scene) => scene.terminal_id === terminalId);
1104
+ const routes = projection.routes.filter((route) => route.terminal_id === terminalId);
1105
+ const sourceInspectionSnapshots = (projection.source_inspection_snapshots || [])
1106
+ .filter((snapshot) => snapshot.terminal_id === terminalId);
1107
+ const changeRequests = projection.change_requests.filter((request) => request.terminal_id === terminalId);
1108
+ const reviewBatches = projection.review_batches.filter((batch) => batch.terminal_id === terminalId);
1109
+ const discardedTombstones = projection.discarded_tombstones.filter((tombstone) => tombstone.terminal_id === terminalId);
1110
+ return {
1111
+ ...projection,
1112
+ pages,
1113
+ review_scenes: reviewScenes,
1114
+ routes,
1115
+ source_inspection_snapshots: sourceInspectionSnapshots,
1116
+ change_requests: changeRequests,
1117
+ review_batches: reviewBatches,
1118
+ discarded_tombstones: discardedTombstones,
1119
+ revision_history: scopedRevisionHistory(projection.revision_history || [], terminalId),
1120
+ component_governance: componentGovernanceForTerminal(projection.component_governance, terminalId),
1121
+ visual_standard_reviews: visualStandardReviewsForTerminal(projection.visual_standard_reviews || [], terminalId),
1122
+ terminal_scope: {
1123
+ terminal_id: terminalId,
1124
+ terminal_name: terminal.terminal_name,
1125
+ mode: "single-terminal",
1126
+ },
1127
+ };
1128
+ }
1129
+
1130
+ export function inspectWorkflowManagerPrototypes(projectRoot, options = {}) {
1131
+ const root = path.resolve(projectRoot);
1132
+ const experience = String(options.experience || ".workflow/experience");
1133
+ const experienceRoot = contained(root, experience, "Experience directory");
1134
+ if (!fs.existsSync(experienceRoot)) return withTerminalScope(emptyPrototypeProjection(root, experienceRoot, experience), options);
1135
+ const registrationFiles = [
1136
+ "visual-system-confirmation.yaml",
1137
+ "visual-reference-manifest.yaml",
1138
+ "prototype.yaml",
1139
+ ].map((name) => path.join(experienceRoot, name));
1140
+ if (registrationFiles.every((file) => !fs.existsSync(file))) {
1141
+ return withTerminalScope(emptyPrototypeProjection(root, experienceRoot, experience), options);
1142
+ }
1143
+ const target = buildPrototypeReviewTarget(root, { experience, revision: options.revision, now: options.now });
1144
+ if (target.mode !== "prototype-revision") return withTerminalScope({
1145
+ schema_version: WORKFLOW_MANAGER_PROTOTYPES_SCHEMA_VERSION,
1146
+ registration_status: "visual-standard-pending",
1147
+ visual_reference: { target_id: target.targetId, checksum: target.targetChecksum, path: target.visualReferencePath },
1148
+ prototype_revision_id: null, prototype_revision_checksum: null,
1149
+ catalog_schema_version: null, terminals: [], catalog_summary: {},
1150
+ visual_standard: null, component_governance: null,
1151
+ pages: [], review_scenes: [], routes: [], change_requests: currentRequests(root), review_batches: batches(root),
1152
+ source_inspection_snapshots: [],
1153
+ discarded_tombstones: tombstones(root), visual_standard_reviews: visualStandardReviews(root), revision_history: revisionHistory(root, experienceRoot, experience),
1154
+ }, options);
1155
+ assertExpectedPrototypeRevisionChecksum(target, options);
1156
+ return withTerminalScope({
1157
+ schema_version: WORKFLOW_MANAGER_PROTOTYPES_SCHEMA_VERSION,
1158
+ registration_status: "registered",
1159
+ prototype_id: target.prototypeId,
1160
+ prototype_revision_id: target.targetId,
1161
+ prototype_revision_checksum: target.targetChecksum,
1162
+ catalog_schema_version: target.catalogSchemaVersion,
1163
+ iteration_id: target.iterationId,
1164
+ revision_ref: target.revisionRelative,
1165
+ review_entrypoint: target.reviewEntrypoint,
1166
+ terminals: target.terminals,
1167
+ catalog_summary: target.catalogSummary,
1168
+ visual_standard: target.visualStandard,
1169
+ component_governance: target.componentGovernance,
1170
+ pages: target.pages,
1171
+ review_scenes: target.reviewScenes,
1172
+ routes: target.routeGraph,
1173
+ source_inspection_snapshots: target.sourceInspectionSnapshots,
1174
+ change_requests: currentRequests(root).filter((item) => item.prototype_revision_id === target.targetId),
1175
+ review_batches: batches(root).filter((item) => item.prototype_revision_id === target.targetId),
1176
+ discarded_tombstones: tombstones(root).filter((item) => item.prototype_revision_id === target.targetId),
1177
+ visual_standard_reviews: visualStandardReviews(root)
1178
+ .filter((item) => item.prototype_revision_id === target.targetId),
1179
+ revision_history: revisionHistory(root, experienceRoot, experience),
1180
+ mutability: "governed",
1181
+ }, options);
1182
+ }
1183
+
1184
+ function sourceSnapshotOption(options) {
1185
+ return options.source_snapshot_id ?? options.sourceSnapshotId ?? null;
1186
+ }
1187
+
1188
+ function sourceSnapshotListContext(target, terminalId) {
1189
+ const terminal = terminalId === "all"
1190
+ ? null
1191
+ : target.terminals.find((item) => item.terminal_id === terminalId);
1192
+ return {
1193
+ terminal_scope: {
1194
+ terminal_id: terminalId,
1195
+ terminal_name: terminal?.terminal_name || undefined,
1196
+ mode: terminalId === "all" ? "all" : "single-terminal",
1197
+ },
1198
+ prototype_revision_id: target.targetId,
1199
+ prototype_revision_checksum: target.targetChecksum,
1200
+ revision_ref: target.revisionRelative,
1201
+ iteration_id: target.iterationId,
1202
+ };
1203
+ }
1204
+
1205
+ function sourceSnapshotMetadata(snapshot) {
1206
+ return Object.fromEntries([
1207
+ "source_snapshot_id", "asset_kind", "page_id", "review_scene_id", "terminal_id",
1208
+ "source_ref", "checksum", "language", "scene_branch",
1209
+ ].map((key) => [key, snapshot[key] ?? null]));
1210
+ }
1211
+
1212
+ /**
1213
+ * Read only the source assets explicitly registered by a Prototype Revision.
1214
+ * The source_snapshot_id is the capability boundary; no arbitrary source path
1215
+ * is accepted by this service.
1216
+ */
1217
+ export function inspectWorkflowManagerPrototypeSourceSnapshots(projectRoot, options = {}) {
1218
+ const root = path.resolve(projectRoot);
1219
+ if (options.source_ref || options.sourceRef || options.sourcePath || options.relative) {
1220
+ throw new PrototypeSourceSnapshotError(
1221
+ "prototype-source-path-forbidden",
1222
+ "Source Inspection Snapshot reads require a registered source_snapshot_id; arbitrary paths are not allowed.",
1223
+ );
1224
+ }
1225
+ const requestedRevision = String(options.revision || "").trim();
1226
+ const expectedChecksum = options.expected_checksum ?? options.expectedChecksum;
1227
+ if (!requestedRevision || expectedChecksum === undefined || expectedChecksum === null || expectedChecksum === "") {
1228
+ throw new PrototypeSourceSnapshotError(
1229
+ "prototype-source-snapshot-unavailable",
1230
+ "Source Inspection Snapshots require an exact revision and expected checksum.",
1231
+ );
1232
+ }
1233
+ const experience = String(options.experience || ".workflow/experience");
1234
+ const target = buildPrototypeReviewTarget(root, {
1235
+ experience,
1236
+ revision: requestedRevision,
1237
+ now: options.now,
1238
+ });
1239
+ if (target.mode !== "prototype-revision") {
1240
+ throw new PrototypeSourceSnapshotError(
1241
+ "prototype-source-snapshot-unavailable",
1242
+ "Source Inspection Snapshots require an exact Prototype revision.",
1243
+ );
1244
+ }
1245
+ assertExpectedPrototypeRevisionChecksum(target, options);
1246
+ const terminalId = requestedTerminalId(options);
1247
+ if (terminalId !== "all" && !target.terminals.some((terminal) => terminal.terminal_id === terminalId)) {
1248
+ throw new PrototypeTerminalScopeError(`Logical Terminal is not registered: ${terminalId}`);
1249
+ }
1250
+ const sourceSnapshotId = sourceSnapshotOption(options);
1251
+ if (sourceSnapshotId && terminalId !== "all") {
1252
+ const registered = (target.sourceInspectionSnapshots || [])
1253
+ .find((item) => item.source_snapshot_id === sourceSnapshotId);
1254
+ if (registered && registered.terminal_id !== terminalId) {
1255
+ throw new PrototypeTerminalScopeError(
1256
+ `Source Inspection Snapshot ${sourceSnapshotId} does not belong to Logical Terminal ${terminalId}.`,
1257
+ );
1258
+ }
1259
+ }
1260
+ const pages = target.pages.filter((page) => terminalId === "all" || page.terminal_id === terminalId);
1261
+ const scenes = target.reviewScenes.filter((scene) => terminalId === "all" || scene.terminal_id === terminalId);
1262
+ const requestedPage = options.page_id ?? options.pageId;
1263
+ const requestedScene = options.review_scene_id ?? options.reviewSceneId;
1264
+ const page = requestedPage ? pages.find((item) => item.page_id === requestedPage) : null;
1265
+ if (requestedPage && !page) {
1266
+ throw new PrototypeSourceSnapshotError("prototype-source-snapshot-invalid", `Page is not registered in the selected Logical Terminal: ${requestedPage}.`);
1267
+ }
1268
+ const scene = requestedScene ? scenes.find((item) => item.review_scene_id === requestedScene) : null;
1269
+ if (requestedScene && !scene) {
1270
+ throw new PrototypeSourceSnapshotError("prototype-source-snapshot-invalid", `Review Scene is not registered in the selected Logical Terminal: ${requestedScene}.`);
1271
+ }
1272
+ if (scene && page && scene.page_id !== page.page_id) {
1273
+ throw new PrototypeSourceSnapshotError("prototype-source-snapshot-invalid", `Review Scene ${requestedScene} does not belong to Page ${requestedPage}.`);
1274
+ }
1275
+ const scopedPageId = requestedPage || scene?.page_id;
1276
+ const snapshots = (target.sourceInspectionSnapshots || [])
1277
+ .filter((snapshot) => terminalId === "all" || snapshot.terminal_id === terminalId)
1278
+ .map(sourceSnapshotMetadata);
1279
+ const scopedSnapshots = snapshots.filter((snapshot) => (!scopedPageId || snapshot.page_id === scopedPageId)
1280
+ && (!requestedScene || !snapshot.review_scene_id || snapshot.review_scene_id === requestedScene));
1281
+ if (!sourceSnapshotId) {
1282
+ return {
1283
+ schema_version: WORKFLOW_MANAGER_PROTOTYPE_SOURCE_SNAPSHOT_SCHEMA_VERSION,
1284
+ ...sourceSnapshotListContext(target, terminalId),
1285
+ source_inspection_snapshots: scopedSnapshots,
1286
+ mutability: "read-only",
1287
+ };
1288
+ }
1289
+ if (typeof sourceSnapshotId !== "string" || !SAFE_ID.test(sourceSnapshotId)) {
1290
+ throw new PrototypeSourceSnapshotError(
1291
+ "prototype-source-snapshot-invalid",
1292
+ "Source Inspection Snapshot identity is invalid.",
1293
+ );
1294
+ }
1295
+ const snapshot = scopedSnapshots.find((item) => item.source_snapshot_id === sourceSnapshotId);
1296
+ if (!snapshot) {
1297
+ const registered = (target.sourceInspectionSnapshots || [])
1298
+ .find((item) => item.source_snapshot_id === sourceSnapshotId);
1299
+ if (registered && terminalId !== "all" && registered.terminal_id !== terminalId) {
1300
+ throw new PrototypeTerminalScopeError(
1301
+ `Source Inspection Snapshot ${sourceSnapshotId} does not belong to Logical Terminal ${terminalId}.`,
1302
+ );
1303
+ }
1304
+ throw new PrototypeSourceSnapshotError(
1305
+ "prototype-source-snapshot-not-registered",
1306
+ `Source Inspection Snapshot is not registered: ${sourceSnapshotId}`,
1307
+ );
1308
+ }
1309
+ if (requestedPage && requestedPage !== snapshot.page_id) {
1310
+ throw new PrototypeSourceSnapshotError(
1311
+ "prototype-source-snapshot-invalid",
1312
+ `Source Inspection Snapshot ${sourceSnapshotId} does not belong to Page ${requestedPage}.`,
1313
+ );
1314
+ }
1315
+ if (requestedScene && snapshot.review_scene_id && requestedScene !== snapshot.review_scene_id) {
1316
+ throw new PrototypeSourceSnapshotError(
1317
+ "prototype-source-snapshot-invalid",
1318
+ `Source Inspection Snapshot ${sourceSnapshotId} does not belong to Review Scene ${requestedScene}.`,
1319
+ );
1320
+ }
1321
+ const sourceFile = containedWithoutSymlinks(
1322
+ target.sourceRoot,
1323
+ snapshot.source_ref,
1324
+ `Source Inspection Snapshot ${sourceSnapshotId} source`,
1325
+ );
1326
+ if (!fs.statSync(sourceFile).isFile()) {
1327
+ throw new PrototypeSourceSnapshotError(
1328
+ "prototype-source-snapshot-invalid",
1329
+ `Source Inspection Snapshot ${sourceSnapshotId} source is not a file.`,
1330
+ );
1331
+ }
1332
+ const bytes = fs.readFileSync(sourceFile);
1333
+ if (digest(bytes) !== snapshot.checksum) {
1334
+ throw new PrototypeSourceSnapshotError(
1335
+ "prototype-source-snapshot-checksum-mismatch",
1336
+ `Source Inspection Snapshot ${sourceSnapshotId} checksum does not match its registered source.`,
1337
+ );
1338
+ }
1339
+ return {
1340
+ schema_version: WORKFLOW_MANAGER_PROTOTYPE_SOURCE_SNAPSHOT_SCHEMA_VERSION,
1341
+ ...sourceSnapshotListContext(target, terminalId),
1342
+ source_snapshot: {
1343
+ ...snapshot,
1344
+ read_only: true,
1345
+ editable: false,
1346
+ executable: false,
1347
+ content: bytes.toString("utf8"),
1348
+ content_encoding: "utf-8",
1349
+ content_type: "text/plain",
1350
+ },
1351
+ mutability: "read-only",
1352
+ };
1353
+ }
1354
+
1355
+ export function inspectWorkflowManagerPrototypeSourceSnapshot(projectRoot, options = {}) {
1356
+ const result = inspectWorkflowManagerPrototypeSourceSnapshots(projectRoot, options);
1357
+ if (!result.source_snapshot) {
1358
+ throw new PrototypeSourceSnapshotError(
1359
+ "prototype-source-snapshot-required",
1360
+ "source_snapshot_id is required when reading Source Inspection Snapshot content.",
1361
+ );
1362
+ }
1363
+ return result;
1364
+ }
1365
+
1366
+ export function resolveWorkflowManagerPrototypeContent(projectRoot, relative, options = {}) {
1367
+ const root = path.resolve(projectRoot);
1368
+ const target = buildPrototypeReviewTarget(root, {
1369
+ experience: options.experience || ".workflow/experience",
1370
+ revision: options.revision,
1371
+ });
1372
+ if (target.mode !== "prototype-revision") throw new Error("Prototype content requires an exact Prototype revision");
1373
+ assertExpectedPrototypeRevisionChecksum(target, options);
1374
+ const file = contained(target.contentRoot, relative, "Prototype content");
1375
+ if (!fs.existsSync(file) || !fs.statSync(file).isFile()) throw new Error("Prototype content was not found");
1376
+ return { file, target_id: target.targetId, target_checksum: target.targetChecksum };
1377
+ }
1378
+
1379
+ export function applyWorkflowManagerPageReview(projectRoot, input = {}, options = {}) {
1380
+ const root = path.resolve(projectRoot);
1381
+ requireHuman(input.actor);
1382
+ const guarded = guardedPrototypeWrite(
1383
+ root,
1384
+ "prototype-page-review-applied",
1385
+ input,
1386
+ input.page_id,
1387
+ () => {
1388
+ const target = buildPrototypeReviewTarget(root, {
1389
+ experience: input.experience || ".workflow/experience",
1390
+ revision: input.revision,
1391
+ });
1392
+ if (target.mode !== "prototype-revision") throw new Error("Page review requires an exact Prototype revision");
1393
+ const { page } = resolveTerminalScopedReviewTarget(target, input);
1394
+ const inspection = formalInspection(root);
1395
+ if (approvedPrototypeGate(inspection, target.targetId, target.targetChecksum)) {
1396
+ throw new Error("Prototype revision is already approved by UX-UI; create a fresh Prototype revision before further review changes");
1397
+ }
1398
+ const receipt = createPrototypePageReviewReceipt(target, page, input);
1399
+ const write = prototypePageReviewReceiptWrite(root, receipt);
1400
+ const completesRevision = receipt.decision === "approved"
1401
+ && target.pages.every((item) => item.page_id === page.page_id || item.review_status === "approved");
1402
+ const pendingUxUi = (inspection.workflow_state?.development_state?.pending_gates || [])
1403
+ .find((gate) => gate?.gate_id === "UX-UI" && gate.iteration_id === target.iterationId);
1404
+ let formal = null;
1405
+ let recorded;
1406
+ if (completesRevision && pendingUxUi) {
1407
+ if (!Number.isInteger(input.expected_scope_revision) || input.expected_scope_revision < 0) {
1408
+ throw new Error("Final Page approval requires expected_scope_revision for the UX-UI Gate");
1409
+ }
1410
+ const evidence = Array.isArray(input.evidence) && input.evidence.length
1411
+ ? input.evidence
1412
+ : pendingUxUi.evidence || [];
1413
+ const args = [
1414
+ "gate", "decide", "--project", root,
1415
+ "--expected-scope-revision", String(input.expected_scope_revision),
1416
+ "--gate-id", "UX-UI", "--decision", "approved",
1417
+ "--actor", input.actor,
1418
+ "--rationale", String(input.comment || input.rationale || "Prototype Page review completed").trim(),
1419
+ ];
1420
+ for (const item of evidence) args.push("--evidence", item);
1421
+ const formalTransaction = prepareWorkflowManagerFormalTransaction(root, {
1422
+ action: "prototype-page-review-applied",
1423
+ subject: page.page_id,
1424
+ input,
1425
+ resultFactory: () => receipt,
1426
+ detailFactory: () => ({
1427
+ terminal_id: page.terminal_id,
1428
+ result_revision: null,
1429
+ result_receipt_id: receipt.receipt_id,
1430
+ result_decision: receipt.decision,
1431
+ affected_identities: [target.targetId, page.page_id, receipt.receipt_id, "UX-UI"],
1432
+ delegated_writer: "delivery-state.gate-decide",
1433
+ }),
1434
+ receiptPrefix: "WMPR",
1435
+ eventDetail: { terminal_id: page.terminal_id },
1436
+ });
1437
+ formal = executeDeliveryStateCommand(args, {
1438
+ yaml,
1439
+ ...(options.stateOptions || {}),
1440
+ pageReviewReceiptWrite: write,
1441
+ managerTransactionWriteFactory: formalTransaction.factory,
1442
+ });
1443
+ recorded = formalTransaction.result();
1444
+ } else {
1445
+ recorded = commitAction(
1446
+ root,
1447
+ "prototype-page-review-applied",
1448
+ input,
1449
+ page.page_id,
1450
+ receipt,
1451
+ [{ file: write.file, content: write.content }],
1452
+ new Date().toISOString(),
1453
+ options,
1454
+ );
1455
+ }
1456
+ return {
1457
+ outcome: "applied",
1458
+ page_review_receipt: receipt,
1459
+ receipt: recorded.receipt,
1460
+ event: recorded.event,
1461
+ ux_ui_gate: formal?.gate || null,
1462
+ formal,
1463
+ };
1464
+ },
1465
+ );
1466
+ if (guarded.replay) {
1467
+ return { outcome: "idempotent-replay", page_review_receipt: guarded.replay.result, receipt: guarded.replay };
1468
+ }
1469
+ return guarded.value;
1470
+ }
1471
+
1472
+ export function applyWorkflowManagerVisualStandardReview(projectRoot, input = {}, options = {}) {
1473
+ const root = path.resolve(projectRoot);
1474
+ requireHuman(input.actor);
1475
+ const guarded = guardedPrototypeWrite(
1476
+ root,
1477
+ "prototype-visual-standard-review-applied",
1478
+ input,
1479
+ input.prototype_revision_id,
1480
+ () => {
1481
+ const { target, terminalId } = targetForVisualStandardReview(root, input);
1482
+ assertPrototypeRevisionWritable(root, target.targetId, target.targetChecksum);
1483
+ if (!new Set(["approved", "changes-requested"]).has(input.decision)) {
1484
+ throw new Error("Visual Standard review decision must be approved or changes-requested");
1485
+ }
1486
+ const comment = String(input.comment || "").trim();
1487
+ if (input.decision === "changes-requested" && !comment) {
1488
+ throw new Error("Visual Standard return requires a non-empty comment");
1489
+ }
1490
+ const timestamp = new Date().toISOString();
1491
+ const review = {
1492
+ schema_version: 1,
1493
+ review_id: `VSR-${crypto.randomUUID()}`,
1494
+ prototype_revision_id: target.targetId,
1495
+ prototype_revision_checksum: target.targetChecksum,
1496
+ visual_standard_ref: target.visualStandard.ref,
1497
+ visual_standard_checksum: target.visualStandard.checksum,
1498
+ terminal_id: terminalId,
1499
+ decision: input.decision,
1500
+ comment: comment || null,
1501
+ actor: input.actor,
1502
+ created_at: timestamp,
1503
+ };
1504
+ review.checksum = requestChecksum(review);
1505
+ const recorded = commitAction(
1506
+ root,
1507
+ "prototype-visual-standard-review-applied",
1508
+ input,
1509
+ target.targetId,
1510
+ review,
1511
+ [{ file: visualStandardReviewFile(root, review.review_id), content: `${JSON.stringify(review, null, 2)}\n` }],
1512
+ timestamp,
1513
+ options,
1514
+ );
1515
+ return {
1516
+ outcome: "applied",
1517
+ visual_standard_review: review,
1518
+ receipt: recorded.receipt,
1519
+ event: recorded.event,
1520
+ };
1521
+ },
1522
+ );
1523
+ if (guarded.replay) {
1524
+ return {
1525
+ outcome: "idempotent-replay",
1526
+ visual_standard_review: guarded.replay.result,
1527
+ receipt: guarded.replay,
1528
+ };
1529
+ }
1530
+ return guarded.value;
1531
+ }
1532
+
1533
+ export function createPrototypeChangeRequest(projectRoot, input = {}, options = {}) {
1534
+ const root = path.resolve(projectRoot);
1535
+ requireHuman(input.actor);
1536
+ const initialReplay = preflightAction(root, "prototype-change-request-created", input);
1537
+ if (initialReplay) return { outcome: "idempotent-replay", change_request: initialReplay.result, receipt: initialReplay };
1538
+ const release = withPrototypeWriteLock(root, "prototype-change-request-created");
1539
+ try {
1540
+ const replay = preflightAction(root, "prototype-change-request-created", input);
1541
+ if (replay) return { outcome: "idempotent-replay", change_request: replay.result, receipt: replay };
1542
+ if (!SAFE_ID.test(String(input.prototype_revision_id || "")) || !SAFE_ID.test(String(input.page_id || ""))) {
1543
+ throw new Error("Prototype revision and Page identities are required");
1544
+ }
1545
+ if (!SHA256.test(String(input.prototype_revision_checksum || "")) || !SHA256.test(String(input.page_checksum || ""))) {
1546
+ throw new Error("Prototype revision and Page checksums are required");
1547
+ }
1548
+ if (!String(input.body || "").trim()) throw new Error("Prototype Change Request body is required");
1549
+ targetForPrototypeRequest(root, input);
1550
+ assertPrototypeRevisionWritable(root, input.prototype_revision_id, input.prototype_revision_checksum);
1551
+ const timestamp = new Date().toISOString();
1552
+ const request = {
1553
+ schema_version: 1,
1554
+ change_request_id: `PCR-${crypto.randomUUID()}`,
1555
+ prototype_revision_id: input.prototype_revision_id,
1556
+ prototype_revision_checksum: input.prototype_revision_checksum,
1557
+ terminal_id: input.terminal_id,
1558
+ page_id: input.page_id,
1559
+ page_checksum: input.page_checksum,
1560
+ review_scene_id: input.review_scene_id || null,
1561
+ revision: 1,
1562
+ status: "draft",
1563
+ body: String(input.body).trim(),
1564
+ attachments: [],
1565
+ actor: input.actor,
1566
+ created_at: timestamp,
1567
+ updated_at: timestamp,
1568
+ review_batch_id: null,
1569
+ execution: { status: "not-started", actor: null, authorization_ref: null, started_at: null },
1570
+ };
1571
+ request.checksum = requestChecksum(request);
1572
+ const recorded = commitAction(
1573
+ root,
1574
+ "prototype-change-request-created",
1575
+ input,
1576
+ null,
1577
+ request,
1578
+ [{ file: requestFile(root, request.change_request_id), content: `${JSON.stringify(request, null, 2)}\n` }],
1579
+ timestamp,
1580
+ options,
1581
+ );
1582
+ return { outcome: "applied", change_request: request, receipt: recorded.receipt, event: recorded.event };
1583
+ } finally {
1584
+ release();
1585
+ }
1586
+ }
1587
+
1588
+ export function applyPrototypeChangeRequest(projectRoot, changeRequestId, input = {}, options = {}) {
1589
+ const root = path.resolve(projectRoot);
1590
+ requireHuman(input.actor);
1591
+ const guarded = guardedPrototypeWrite(root, "prototype-change-request-edited", input, changeRequestId, () => {
1592
+ const { file, request } = readRequest(root, changeRequestId);
1593
+ assertStoredPrototypeRequestTerminal(root, request);
1594
+ assertPrototypeRevisionWritable(root, request.prototype_revision_id, request.prototype_revision_checksum);
1595
+ if (request.review_batch_id) throw new Error("Prototype Change Request belongs to a finalized review batch");
1596
+ if (request.execution?.status === "started") throw new Error("Prototype Change Request execution has started");
1597
+ if (request.revision !== input.expected_revision) throw new Error("Prototype Change Request revision mismatch");
1598
+ if (!String(input.body || "").trim()) throw new Error("Prototype Change Request body is required");
1599
+ const timestamp = new Date().toISOString();
1600
+ request.body = String(input.body).trim();
1601
+ request.revision += 1;
1602
+ request.updated_at = timestamp;
1603
+ request.checksum = requestChecksum(request);
1604
+ const recorded = commitAction(root, "prototype-change-request-edited", input, changeRequestId, request,
1605
+ [{ file, content: `${JSON.stringify(request, null, 2)}\n` }], timestamp, options);
1606
+ return { outcome: "applied", change_request: request, receipt: recorded.receipt, event: recorded.event };
1607
+ });
1608
+ if (guarded.replay) return { outcome: "idempotent-replay", change_request: guarded.replay.result, receipt: guarded.replay };
1609
+ return guarded.value;
1610
+ }
1611
+
1612
+ function attachmentBytes(root, input) {
1613
+ if (typeof input.content_base64 === "string") return Buffer.from(input.content_base64, "base64");
1614
+ if (input.source_path) {
1615
+ const source = contained(root, input.source_path, "Prototype attachment source");
1616
+ const importRoot = contained(root, ".workflow/imports", "Prototype import directory");
1617
+ if (!inside(importRoot, source)) throw new Error("Prototype attachment source must remain inside .workflow/imports");
1618
+ if (!fs.existsSync(source) || !fs.statSync(source).isFile()) throw new Error("Prototype attachment source is missing");
1619
+ return fs.readFileSync(source);
1620
+ }
1621
+ throw new Error("Prototype attachment requires source_path or content_base64");
1622
+ }
1623
+
1624
+ export function appendPrototypeChangeRequestAttachment(projectRoot, changeRequestId, input = {}, options = {}) {
1625
+ const root = path.resolve(projectRoot);
1626
+ requireHuman(input.actor);
1627
+ const guarded = guardedPrototypeWrite(root, "prototype-change-request-attachment-added", input, changeRequestId, () => {
1628
+ const { file, request } = readRequest(root, changeRequestId);
1629
+ assertStoredPrototypeRequestTerminal(root, request);
1630
+ assertPrototypeRevisionWritable(root, request.prototype_revision_id, request.prototype_revision_checksum);
1631
+ if (request.review_batch_id) throw new Error("Prototype Change Request belongs to a finalized review batch");
1632
+ if (request.execution?.status === "started") throw new Error("Prototype Change Request execution has started");
1633
+ if (request.revision !== input.expected_revision) throw new Error("Prototype Change Request revision mismatch");
1634
+ if (!new Set(["image/png", "image/jpeg", "image/webp"]).has(input.media_type)) {
1635
+ throw new Error("Prototype review attachment must be PNG, JPEG, or WebP");
1636
+ }
1637
+ const bytes = attachmentBytes(root, input);
1638
+ if (!bytes.length || bytes.length > 10 * 1024 * 1024) throw new Error("Prototype review attachment must be between 1 byte and 10 MiB");
1639
+ const extension = { "image/png": "png", "image/jpeg": "jpg", "image/webp": "webp" }[input.media_type];
1640
+ const attachmentId = `PCRA-${crypto.randomUUID()}`;
1641
+ const relative = `${ATTACHMENT_ROOT}/${changeRequestId}/${attachmentId}.${extension}`;
1642
+ const target = contained(root, relative, "Prototype review attachment");
1643
+ const timestamp = new Date().toISOString();
1644
+ request.attachments.push({ attachment_id: attachmentId, path: relative, media_type: input.media_type, checksum: digest(bytes), size_bytes: bytes.length, created_at: timestamp });
1645
+ request.revision += 1;
1646
+ request.updated_at = timestamp;
1647
+ request.checksum = requestChecksum(request);
1648
+ const recorded = commitAction(root, "prototype-change-request-attachment-added", input, changeRequestId, request, [
1649
+ { file: target, content: bytes },
1650
+ { file, content: `${JSON.stringify(request, null, 2)}\n` },
1651
+ ], timestamp, options);
1652
+ return { outcome: "applied", change_request: request, receipt: recorded.receipt, event: recorded.event };
1653
+ });
1654
+ if (guarded.replay) return { outcome: "idempotent-replay", change_request: guarded.replay.result, receipt: guarded.replay };
1655
+ return guarded.value;
1656
+ }
1657
+
1658
+ export function inspectWorkflowManagerPrototypeAttachment(projectRoot, input = {}) {
1659
+ const root = path.resolve(projectRoot);
1660
+ const attachmentId = String(input.attachment_id || "");
1661
+ if (!SAFE_ID.test(attachmentId)) throw new Error("Prototype review attachment identity is invalid");
1662
+ if (!SHA256.test(String(input.expected_checksum || ""))) {
1663
+ throw new Error("Prototype review attachment expected_checksum is required");
1664
+ }
1665
+ let request = null;
1666
+ let metadata = null;
1667
+ for (const candidate of currentRequests(root)) {
1668
+ const match = (candidate.attachments || []).find((item) => item.attachment_id === attachmentId);
1669
+ if (!match) continue;
1670
+ if (metadata) throw new Error(`Prototype review attachment identity is duplicated: ${attachmentId}`);
1671
+ request = candidate;
1672
+ metadata = match;
1673
+ }
1674
+ if (!request || !metadata) throw new Error(`Prototype review attachment does not exist: ${attachmentId}`);
1675
+ const requestAttachmentRoot = contained(
1676
+ root,
1677
+ `${ATTACHMENT_ROOT}/${request.change_request_id}`,
1678
+ "Prototype Change Request attachment directory",
1679
+ );
1680
+ const file = contained(root, metadata.path, "Prototype review attachment");
1681
+ if (!inside(requestAttachmentRoot, file)) throw new Error("Prototype review attachment path does not match its request");
1682
+ if (!fs.existsSync(file) || !fs.statSync(file).isFile()) throw new Error("Prototype review attachment content is missing");
1683
+ const bytes = fs.readFileSync(file);
1684
+ if (!bytes.length || bytes.length > 10 * 1024 * 1024) throw new Error("Prototype review attachment size is invalid");
1685
+ const checksum = digest(bytes);
1686
+ if (checksum !== metadata.checksum || checksum !== input.expected_checksum) {
1687
+ throw new Error("Prototype review attachment checksum mismatch");
1688
+ }
1689
+ return {
1690
+ schema_version: "workflow-manager.attachment.v1",
1691
+ lifecycle_status: request.status,
1692
+ mutability: "immutable-evidence",
1693
+ change_request_id: request.change_request_id,
1694
+ review_batch_id: request.review_batch_id,
1695
+ prototype_revision_id: request.prototype_revision_id,
1696
+ terminal_id: request.terminal_id,
1697
+ page_id: request.page_id,
1698
+ review_scene_id: request.review_scene_id,
1699
+ attachment_id: attachmentId,
1700
+ media_type: metadata.media_type,
1701
+ checksum,
1702
+ size_bytes: bytes.length,
1703
+ content_encoding: "base64",
1704
+ content_base64: bytes.toString("base64"),
1705
+ };
1706
+ }
1707
+
1708
+ export function finalizePrototypeReviewBatch(projectRoot, input = {}, options = {}) {
1709
+ const root = path.resolve(projectRoot);
1710
+ requireHuman(input.actor);
1711
+ const initialReplay = preflightAction(root, "prototype-review-batch-finalized", input);
1712
+ if (initialReplay) return { outcome: "idempotent-replay", review_batch: initialReplay.result, receipt: initialReplay };
1713
+ const release = withPrototypeWriteLock(root, "prototype-review-batch-finalized");
1714
+ try {
1715
+ const replay = preflightAction(root, "prototype-review-batch-finalized", input);
1716
+ if (replay) return { outcome: "idempotent-replay", review_batch: replay.result, receipt: replay };
1717
+ if (!Array.isArray(input.change_request_ids) || !input.change_request_ids.length) throw new Error("Review Batch requires change_request_ids");
1718
+ requireTerminalIdentity(input.terminal_id);
1719
+ assertPrototypeRevisionWritable(root, input.prototype_revision_id, input.prototype_revision_checksum);
1720
+ const requests = input.change_request_ids.map((id) => readRequest(root, id));
1721
+ for (const { request } of requests) {
1722
+ assertStoredPrototypeRequestTerminal(root, request);
1723
+ if (request.prototype_revision_id !== input.prototype_revision_id || request.prototype_revision_checksum !== input.prototype_revision_checksum
1724
+ || request.terminal_id !== input.terminal_id || request.page_id !== input.page_id) {
1725
+ throw new Error("Review Batch requests must bind the exact Prototype revision, Logical Terminal, and Page");
1726
+ }
1727
+ if (request.review_batch_id) throw new Error("Prototype Change Request already belongs to a finalized review batch");
1728
+ if (request.execution?.status === "started") throw new Error("Prototype Change Request execution has started");
1729
+ }
1730
+ const timestamp = new Date().toISOString();
1731
+ const batch = {
1732
+ schema_version: 1,
1733
+ review_batch_id: `PRB-${crypto.randomUUID()}`,
1734
+ prototype_revision_id: input.prototype_revision_id,
1735
+ prototype_revision_checksum: input.prototype_revision_checksum,
1736
+ terminal_id: input.terminal_id,
1737
+ page_id: input.page_id,
1738
+ status: "finalized",
1739
+ actor: input.actor,
1740
+ finalized_at: timestamp,
1741
+ change_requests: requests.map(({ request }) => requestSnapshot(request)),
1742
+ };
1743
+ batch.checksum = digest(JSON.stringify(batch));
1744
+ const writes = [{
1745
+ file: contained(root, `${BATCH_ROOT}/${batch.review_batch_id}.json`, "Review Batch path"),
1746
+ content: `${JSON.stringify(batch, null, 2)}\n`,
1747
+ }];
1748
+ for (const { file, request } of requests) {
1749
+ request.status = "finalized";
1750
+ request.review_batch_id = batch.review_batch_id;
1751
+ request.updated_at = timestamp;
1752
+ request.checksum = requestChecksum(request);
1753
+ writes.push({ file, content: `${JSON.stringify(request, null, 2)}\n` });
1754
+ }
1755
+ const recorded = commitAction(root, "prototype-review-batch-finalized", input, null, batch, writes, timestamp, options);
1756
+ return { outcome: "applied", review_batch: batch, receipt: recorded.receipt, event: recorded.event };
1757
+ } finally {
1758
+ release();
1759
+ }
1760
+ }
1761
+
1762
+ export function markPrototypeChangeRequestExecution(projectRoot, changeRequestId, input = {}, options = {}) {
1763
+ const root = path.resolve(projectRoot);
1764
+ if (!/^agent:[A-Za-z0-9._@/-]+$/.test(String(input.actor || ""))) throw new Error("Execution actor must use agent:<identity>");
1765
+ if (!String(input.authorization_ref || "").trim()) throw new Error("Prototype redesign authorization reference is required");
1766
+ const guarded = guardedPrototypeWrite(root, "prototype-change-request-execution-started", input, changeRequestId, () => {
1767
+ const { file, request } = readRequest(root, changeRequestId);
1768
+ assertStoredPrototypeRequestTerminal(root, request);
1769
+ assertPrototypeRevisionWritable(root, request.prototype_revision_id, request.prototype_revision_checksum);
1770
+ if (request.revision !== input.expected_revision) throw new Error("Prototype Change Request revision mismatch");
1771
+ if (request.execution?.status === "started") throw new Error("Prototype Change Request execution already started with another request");
1772
+ const timestamp = new Date().toISOString();
1773
+ request.execution = { status: "started", actor: input.actor, authorization_ref: input.authorization_ref, started_at: timestamp };
1774
+ request.revision += 1;
1775
+ request.status = "in-execution";
1776
+ request.updated_at = timestamp;
1777
+ request.checksum = requestChecksum(request);
1778
+ const recorded = commitAction(root, "prototype-change-request-execution-started", input, changeRequestId, request,
1779
+ [{ file, content: `${JSON.stringify(request, null, 2)}\n` }], timestamp, options);
1780
+ return { outcome: "applied", change_request: request, receipt: recorded.receipt, event: recorded.event };
1781
+ });
1782
+ if (guarded.replay) return { outcome: "idempotent-replay", change_request: guarded.replay.result, receipt: guarded.replay };
1783
+ return guarded.value;
1784
+ }
1785
+
1786
+ export function discardPrototypeChangeRequest(projectRoot, changeRequestId, input = {}, options = {}) {
1787
+ const root = path.resolve(projectRoot);
1788
+ requireHuman(input.actor);
1789
+ const guarded = guardedPrototypeWrite(root, "prototype-change-request-discarded", input, changeRequestId, () => {
1790
+ const { file, request } = readRequest(root, changeRequestId);
1791
+ assertStoredPrototypeRequestTerminal(root, request);
1792
+ assertPrototypeRevisionWritable(root, request.prototype_revision_id, request.prototype_revision_checksum);
1793
+ if (request.revision !== input.expected_revision) throw new Error("Prototype Change Request revision mismatch");
1794
+ if (request.execution?.status === "started") throw new Error("Prototype Change Request execution has started");
1795
+ if (!String(input.rationale || "").trim()) throw new Error("Discard rationale is required");
1796
+ const timestamp = new Date().toISOString();
1797
+ const tombstone = {
1798
+ schema_version: 1,
1799
+ change_request_id: request.change_request_id,
1800
+ prototype_revision_id: request.prototype_revision_id,
1801
+ terminal_id: request.terminal_id,
1802
+ page_id: request.page_id,
1803
+ review_batch_id: request.review_batch_id,
1804
+ status: "discarded",
1805
+ discarded_by: input.actor,
1806
+ discarded_at: timestamp,
1807
+ rationale: String(input.rationale).trim(),
1808
+ };
1809
+ tombstone.checksum = digest(JSON.stringify(tombstone));
1810
+ const discardedRequest = {
1811
+ schema_version: request.schema_version,
1812
+ change_request_id: request.change_request_id,
1813
+ prototype_revision_id: request.prototype_revision_id,
1814
+ prototype_revision_checksum: request.prototype_revision_checksum,
1815
+ terminal_id: request.terminal_id,
1816
+ page_id: request.page_id,
1817
+ page_checksum: request.page_checksum,
1818
+ review_scene_id: request.review_scene_id,
1819
+ revision: request.revision + 1,
1820
+ status: "discarded",
1821
+ body: null,
1822
+ attachments: [],
1823
+ actor: request.actor,
1824
+ created_at: request.created_at,
1825
+ updated_at: timestamp,
1826
+ review_batch_id: request.review_batch_id,
1827
+ execution: request.execution,
1828
+ discarded_tombstone_checksum: tombstone.checksum,
1829
+ };
1830
+ discardedRequest.checksum = requestChecksum(discardedRequest);
1831
+ const attachmentFiles = (request.attachments || []).map((attachment) =>
1832
+ contained(root, attachment.path, "Prototype review attachment"));
1833
+ const tombstoneFile = contained(root, `${TOMBSTONE_ROOT}/${changeRequestId}.json`, "discard tombstone path");
1834
+ const recorded = commitAction(root, "prototype-change-request-discarded", input, changeRequestId, tombstone, [
1835
+ { file, content: `${JSON.stringify(discardedRequest, null, 2)}\n` },
1836
+ ...attachmentFiles.filter((attachmentFile) => fs.existsSync(attachmentFile))
1837
+ .map((attachmentFile) => ({ file: attachmentFile, content: Buffer.alloc(0) })),
1838
+ { file: tombstoneFile, content: `${JSON.stringify(tombstone, null, 2)}\n` },
1839
+ ], timestamp, options);
1840
+ // Cleanup is intentionally after the atomic scrub: an interrupted cleanup cannot expose discarded content.
1841
+ fs.rmSync(file, { force: true });
1842
+ for (const attachmentFile of attachmentFiles) fs.rmSync(attachmentFile, { force: true });
1843
+ return { outcome: "applied", tombstone, receipt: recorded.receipt, event: recorded.event };
1844
+ });
1845
+ if (guarded.replay) return { outcome: "idempotent-replay", tombstone: guarded.replay.result, receipt: guarded.replay };
1846
+ return guarded.value;
1847
+ }