@thebackstoryis/engineering-with-ai 0.2.9

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 (334) hide show
  1. package/Docs/README.md +50 -0
  2. package/Docs/adoption/consultancy-and-multi-project-rollout.md +135 -0
  3. package/Docs/adoption/non-technical-team-guide.md +126 -0
  4. package/Docs/archaeology-technology-and-hosting-discovery.md +212 -0
  5. package/Docs/blast-radius-and-impact-routing-guide.md +325 -0
  6. package/Docs/blueprints/internal-blueprint-catalogue.md +146 -0
  7. package/Docs/blueprints/maintaining-organisation-blueprints.md +154 -0
  8. package/Docs/blueprints/validation-and-troubleshooting.md +168 -0
  9. package/Docs/cli-reference.md +113 -0
  10. package/Docs/completed-phase-evidence-amendments.md +74 -0
  11. package/Docs/consultancy-network-rollout-control-plane-guide.md +202 -0
  12. package/Docs/context-aware-delivery-companion-guide.md +198 -0
  13. package/Docs/context-aware-delivery-companion-user-guide.md +184 -0
  14. package/Docs/context-management-and-token-efficiency.md +113 -0
  15. package/Docs/design-systems/design-system-implementation-guide.md +85 -0
  16. package/Docs/design-systems/design-system-pack-authoring-guide.md +95 -0
  17. package/Docs/design-systems/design-system-review-guide.md +51 -0
  18. package/Docs/design-systems/design-system-user-guide.md +96 -0
  19. package/Docs/design-systems/product-owner-guide.md +49 -0
  20. package/Docs/designing-organisation-blueprint-packs.md +384 -0
  21. package/Docs/developer-delivery-guide.md +224 -0
  22. package/Docs/error-reporting-guide.md +110 -0
  23. package/Docs/error-reporting-provider-guide.md +49 -0
  24. package/Docs/examples/error-report-adapter.md +70 -0
  25. package/Docs/examples/meeting-review.md +76 -0
  26. package/Docs/examples/minimal-design-system.md +67 -0
  27. package/Docs/examples/prototype-review-inputs.md +175 -0
  28. package/Docs/examples/reproducible-archaeology-depth-example.md +144 -0
  29. package/Docs/examples/test-scenario-input.md +68 -0
  30. package/Docs/examples/worked-examples.md +147 -0
  31. package/Docs/existing-project-onboarding-guide.md +214 -0
  32. package/Docs/explanation/core-concepts.md +26 -0
  33. package/Docs/explanation/delivery-workflow.md +48 -0
  34. package/Docs/governance/governance-team-guide.md +139 -0
  35. package/Docs/governed-starter-project-materialisation-guide.md +284 -0
  36. package/Docs/guide-catalogue.md +117 -0
  37. package/Docs/guided-discovery-facilitator-guide.md +172 -0
  38. package/Docs/guided-intent-workspace-guide.md +109 -0
  39. package/Docs/guided-phase-evidence-drafting-guide.md +119 -0
  40. package/Docs/human-approval-and-assurance-guide.md +146 -0
  41. package/Docs/knowledge-proposals-implementer-guide.md +106 -0
  42. package/Docs/knowledge-proposals-user-guide.md +247 -0
  43. package/Docs/maintainers/context-benchmarks.md +29 -0
  44. package/Docs/maintainers/contributing.md +58 -0
  45. package/Docs/maintainers/evidence-depth-acceptance.md +72 -0
  46. package/Docs/maintainers/verification-walkthroughs.md +104 -0
  47. package/Docs/meeting-evidence-implementer-guide.md +132 -0
  48. package/Docs/meeting-evidence-user-guide.md +200 -0
  49. package/Docs/operations/dashboard-and-delivery-state.md +153 -0
  50. package/Docs/operations/dashboard-configuration.md +87 -0
  51. package/Docs/operations/installation-updating-and-entitlements.md +135 -0
  52. package/Docs/operations/premium-personas-setup.md +76 -0
  53. package/Docs/operations/troubleshooting-and-recovery.md +205 -0
  54. package/Docs/organisation-rollout-guide.md +142 -0
  55. package/Docs/persona-entitlement-provider-guide.md +199 -0
  56. package/Docs/persona-guided-prototype-iteration.md +129 -0
  57. package/Docs/personas/organisation-specific-personas.md +103 -0
  58. package/Docs/personas/persona-authoring-cookbook.md +176 -0
  59. package/Docs/personas/persona-engagement-ui.md +133 -0
  60. package/Docs/personas/persona-governance.md +118 -0
  61. package/Docs/platform-export-analysis-guide.md +336 -0
  62. package/Docs/policies/governance-owner-guide.md +36 -0
  63. package/Docs/policies/implementation-guide.md +42 -0
  64. package/Docs/policies/organisation-policy-design-gates.md +58 -0
  65. package/Docs/policies/policy-pack-authoring-guide.md +108 -0
  66. package/Docs/policies/product-owner-guide.md +43 -0
  67. package/Docs/policies/technical-owner-guide.md +37 -0
  68. package/Docs/product-owner-guide.md +327 -0
  69. package/Docs/project-portfolio-orchestration-guide.md +199 -0
  70. package/Docs/quality/manual-qa-and-acceptance.md +162 -0
  71. package/Docs/quality/persona-driven-test-scenarios.md +172 -0
  72. package/Docs/quality/reproducible-archaeology-depth-review-checklist.md +89 -0
  73. package/Docs/reference/capabilities-and-project-layout.md +678 -0
  74. package/Docs/reference/cli-and-configuration.md +398 -0
  75. package/Docs/reference/contributions-api.md +23 -0
  76. package/Docs/reference/security-adapter-authoring.md +81 -0
  77. package/Docs/reference/starter-adapter-authoring.md +74 -0
  78. package/Docs/repository-source-map-guide.md +381 -0
  79. package/Docs/reproducible-archaeology-and-discovery-depth.md +292 -0
  80. package/Docs/screen-prototype-creation-guide.md +324 -0
  81. package/Docs/security-validation-guide.md +353 -0
  82. package/Docs/solution-readiness-review-guide.md +123 -0
  83. package/Docs/standards/project-standards-authoring.md +157 -0
  84. package/Docs/team-hub-guide.md +162 -0
  85. package/Docs/team-hub-resource-registry-guide.md +167 -0
  86. package/Docs/tutorials/first-delivery.md +83 -0
  87. package/Docs/tutorials/first-session.md +62 -0
  88. package/Docs/using-lifecycle-hooks.md +381 -0
  89. package/Docs/working-with-personas.md +274 -0
  90. package/LICENSE +165 -0
  91. package/README.md +96 -0
  92. package/agents-src/claude/ewai-security-reviewer.md +15 -0
  93. package/bin/ewai +5 -0
  94. package/config/archaeology-record-families.yaml +59 -0
  95. package/config/delivery-artifacts.yaml +121 -0
  96. package/config/delivery-stages.yaml +77 -0
  97. package/config/design-system.schema.json +46 -0
  98. package/config/error-reporting.schema.json +79 -0
  99. package/config/evidence-depth.schema.json +53 -0
  100. package/config/intent.schema.json +90 -0
  101. package/config/knowledge-proposals-proposal.schema.json +34 -0
  102. package/config/lifecycle-event.schema.json +68 -0
  103. package/config/lifecycle-handler.schema.json +45 -0
  104. package/config/lifecycle-hook-ack.schema.json +19 -0
  105. package/config/meeting-evidence-candidate.schema.json +102 -0
  106. package/config/organisation-policy.schema.json +137 -0
  107. package/config/pack.schema.json +250 -0
  108. package/config/persona-pack.schema.json +21 -0
  109. package/config/persona.schema.json +17 -0
  110. package/config/policy-evaluation.schema.json +77 -0
  111. package/config/policy-facts.schema.json +140 -0
  112. package/config/portfolio.schema.json +68 -0
  113. package/config/project.schema.json +313 -0
  114. package/config/prototype-iteration.schema.json +128 -0
  115. package/config/rollout.schema.json +87 -0
  116. package/config/security-adapter.schema.json +31 -0
  117. package/config/security-scan-request.schema.json +64 -0
  118. package/config/security-scan-response.schema.json +52 -0
  119. package/config/security-validation-policy.schema.json +74 -0
  120. package/config/starter-source-acknowledgement.schema.json +13 -0
  121. package/config/starter-source-adapter.schema.json +38 -0
  122. package/config/starter-source-request.schema.json +61 -0
  123. package/package.json +77 -0
  124. package/packs/core/pack.yaml +7 -0
  125. package/packs/design-systems/default/experience-promise.md +9 -0
  126. package/packs/design-systems/default/intentional-review.md +10 -0
  127. package/packs/design-systems/default/interaction-and-entry.md +9 -0
  128. package/packs/design-systems/default/meaningful-content-and-states.md +9 -0
  129. package/packs/design-systems/default/pack.yaml +44 -0
  130. package/packs/design-systems/default/principles.md +10 -0
  131. package/packs/personas/core/pack.yaml +7 -0
  132. package/packs/personas/core/personas/archaeologist.md +37 -0
  133. package/packs/personas/core/personas/end-user.md +17 -0
  134. package/packs/personas/core/personas/maintainer.md +17 -0
  135. package/packs/personas/core/personas/operator.md +17 -0
  136. package/packs/personas/core/personas/specs-knowledge-curator.md +35 -0
  137. package/packs/technologies/laravel/pack.yaml +30 -0
  138. package/packs/technologies/laravel-nuxt/pack.yaml +30 -0
  139. package/packs/technologies/nuxt/pack.yaml +30 -0
  140. package/packs/technologies/power-platform/pack.yaml +31 -0
  141. package/packs/technologies/salesforce/pack.yaml +25 -0
  142. package/public/app.js +4896 -0
  143. package/public/apple-touch-icon.png +0 -0
  144. package/public/assets/backstory-icon.png +0 -0
  145. package/public/dashboard-navigation.js +98 -0
  146. package/public/favicon-16.png +0 -0
  147. package/public/favicon-32.png +0 -0
  148. package/public/favicon.ico +0 -0
  149. package/public/index.html +789 -0
  150. package/public/styles.css +2693 -0
  151. package/public/team-hub/app.js +202 -0
  152. package/public/team-hub/index.html +79 -0
  153. package/public/team-hub/styles.css +90 -0
  154. package/scripts/publication-check.mjs +140 -0
  155. package/scripts/setup.mjs +21 -0
  156. package/skills-src/ewai-archaeology/SKILL.md +334 -0
  157. package/skills-src/ewai-archaeology/agents/openai.yaml +4 -0
  158. package/skills-src/ewai-archaeology/references/archaeology-contract.md +201 -0
  159. package/skills-src/ewai-archaeology/references/lifecycle-reconstruction.md +177 -0
  160. package/skills-src/ewai-archaeology/references/maximum-detail-reconstruction.md +97 -0
  161. package/skills-src/ewai-archaeology/references/model-routing.md +26 -0
  162. package/skills-src/ewai-architecture/SKILL.md +108 -0
  163. package/skills-src/ewai-architecture/agents/openai.yaml +4 -0
  164. package/skills-src/ewai-architecture/references/architecture-contract.md +176 -0
  165. package/skills-src/ewai-context/SKILL.md +68 -0
  166. package/skills-src/ewai-context/agents/openai.yaml +4 -0
  167. package/skills-src/ewai-context-import/SKILL.md +118 -0
  168. package/skills-src/ewai-context-import/agents/openai.yaml +4 -0
  169. package/skills-src/ewai-context-import/references/context-import-contract.md +106 -0
  170. package/skills-src/ewai-dashboard-configuration/SKILL.md +20 -0
  171. package/skills-src/ewai-deliver/SKILL.md +122 -0
  172. package/skills-src/ewai-deliver/references/delivery-evidence.md +92 -0
  173. package/skills-src/ewai-deliver/references/phase-routing.md +31 -0
  174. package/skills-src/ewai-design-system-apply/SKILL.md +27 -0
  175. package/skills-src/ewai-design-system-apply/agents/openai.yaml +4 -0
  176. package/skills-src/ewai-design-system-apply/references/application-contract.md +36 -0
  177. package/skills-src/ewai-design-system-author/SKILL.md +28 -0
  178. package/skills-src/ewai-design-system-author/agents/openai.yaml +4 -0
  179. package/skills-src/ewai-design-system-author/references/authoring-contract.md +38 -0
  180. package/skills-src/ewai-design-system-review/SKILL.md +26 -0
  181. package/skills-src/ewai-design-system-review/agents/openai.yaml +4 -0
  182. package/skills-src/ewai-design-system-review/references/review-contract.md +40 -0
  183. package/skills-src/ewai-error-reporting/SKILL.md +46 -0
  184. package/skills-src/ewai-error-reporting/agents/openai.yaml +4 -0
  185. package/skills-src/ewai-error-reporting/references/provider-contract.md +74 -0
  186. package/skills-src/ewai-evidence-depth/SKILL.md +72 -0
  187. package/skills-src/ewai-evidence-depth/agents/openai.yaml +4 -0
  188. package/skills-src/ewai-evidence-depth/references/evidence-depth-contract.md +127 -0
  189. package/skills-src/ewai-intent/SKILL.md +68 -0
  190. package/skills-src/ewai-intent/agents/openai.yaml +4 -0
  191. package/skills-src/ewai-intent/references/intent-contract.md +42 -0
  192. package/skills-src/ewai-knowledge-proposals/SKILL.md +105 -0
  193. package/skills-src/ewai-knowledge-proposals/agents/openai.yaml +4 -0
  194. package/skills-src/ewai-knowledge-proposals/references/proposal-contract.md +59 -0
  195. package/skills-src/ewai-meeting-evidence/SKILL.md +106 -0
  196. package/skills-src/ewai-meeting-evidence/agents/openai.yaml +4 -0
  197. package/skills-src/ewai-meeting-evidence/references/candidate-contract.md +64 -0
  198. package/skills-src/ewai-organisation-policy/SKILL.md +62 -0
  199. package/skills-src/ewai-organisation-policy/agents/openai.yaml +4 -0
  200. package/skills-src/ewai-organisation-policy/references/policy-contract.md +94 -0
  201. package/skills-src/ewai-palace-housekeeping/SKILL.md +55 -0
  202. package/skills-src/ewai-palace-housekeeping/agents/openai.yaml +4 -0
  203. package/skills-src/ewai-persona-entitlement/SKILL.md +60 -0
  204. package/skills-src/ewai-persona-entitlement/agents/openai.yaml +4 -0
  205. package/skills-src/ewai-phase-evidence/SKILL.md +79 -0
  206. package/skills-src/ewai-phase-evidence/agents/openai.yaml +4 -0
  207. package/skills-src/ewai-pipeline/SKILL.md +130 -0
  208. package/skills-src/ewai-pipeline/agents/openai.yaml +4 -0
  209. package/skills-src/ewai-pipeline/references/cli.md +86 -0
  210. package/skills-src/ewai-pipeline/references/specs-contract.md +16 -0
  211. package/skills-src/ewai-portfolio/SKILL.md +70 -0
  212. package/skills-src/ewai-portfolio/agents/openai.yaml +4 -0
  213. package/skills-src/ewai-portfolio/references/portfolio-contract.md +67 -0
  214. package/skills-src/ewai-project-discovery/SKILL.md +95 -0
  215. package/skills-src/ewai-project-discovery/agents/openai.yaml +4 -0
  216. package/skills-src/ewai-project-discovery/references/discovery-contract.md +34 -0
  217. package/skills-src/ewai-prototype-iteration/SKILL.md +30 -0
  218. package/skills-src/ewai-prototype-iteration/agents/openai.yaml +4 -0
  219. package/skills-src/ewai-prototype-iteration/references/review-contract.md +49 -0
  220. package/skills-src/ewai-retro/SKILL.md +48 -0
  221. package/skills-src/ewai-retro/agents/openai.yaml +4 -0
  222. package/skills-src/ewai-retro/references/asset-routing.md +14 -0
  223. package/skills-src/ewai-rollout/SKILL.md +74 -0
  224. package/skills-src/ewai-rollout/agents/openai.yaml +4 -0
  225. package/skills-src/ewai-rollout/references/rollout-contract.md +74 -0
  226. package/skills-src/ewai-shape-intents/SKILL.md +84 -0
  227. package/skills-src/ewai-shape-intents/agents/openai.yaml +4 -0
  228. package/skills-src/ewai-shape-intents/references/intent-mapping-contract.md +109 -0
  229. package/skills-src/ewai-solution-readiness/SKILL.md +55 -0
  230. package/skills-src/ewai-solution-readiness/agents/openai.yaml +4 -0
  231. package/skills-src/ewai-standards-check/SKILL.md +93 -0
  232. package/skills-src/ewai-standards-check/agents/openai.yaml +4 -0
  233. package/skills-src/ewai-standards-check/references/report-contract.md +116 -0
  234. package/skills-src/ewai-test-scenarios/SKILL.md +94 -0
  235. package/skills-src/ewai-test-scenarios/agents/openai.yaml +4 -0
  236. package/skills-src/ewai-test-scenarios/references/scenario-contract.md +88 -0
  237. package/src/afk-worker.mjs +16 -0
  238. package/src/archaeology.mjs +1333 -0
  239. package/src/checkin.mjs +261 -0
  240. package/src/cli.mjs +2427 -0
  241. package/src/companion-guidance.mjs +257 -0
  242. package/src/companion-opening.mjs +62 -0
  243. package/src/companion.mjs +256 -0
  244. package/src/context.mjs +210 -0
  245. package/src/dashboard-preferences.mjs +80 -0
  246. package/src/delivery-artifacts.mjs +204 -0
  247. package/src/delivery-documents.mjs +248 -0
  248. package/src/delivery-gates.mjs +317 -0
  249. package/src/delivery.mjs +1433 -0
  250. package/src/design-system-application.mjs +291 -0
  251. package/src/design-system-authoring.mjs +101 -0
  252. package/src/design-systems.mjs +466 -0
  253. package/src/discovery.mjs +1314 -0
  254. package/src/error-reporting.mjs +323 -0
  255. package/src/evidence-depth.mjs +543 -0
  256. package/src/execution-state.mjs +243 -0
  257. package/src/install.mjs +166 -0
  258. package/src/intent-dependencies.mjs +117 -0
  259. package/src/intent-maps.mjs +402 -0
  260. package/src/intents.mjs +747 -0
  261. package/src/knowledge-proposals.mjs +717 -0
  262. package/src/launcher.mjs +51 -0
  263. package/src/meeting-evidence.mjs +703 -0
  264. package/src/network-rollout.mjs +386 -0
  265. package/src/organisation-blueprints.mjs +438 -0
  266. package/src/organisation-policies.mjs +448 -0
  267. package/src/packs.mjs +44 -0
  268. package/src/paths.mjs +62 -0
  269. package/src/persona-entitlements.mjs +438 -0
  270. package/src/persona-licence-config.mjs +98 -0
  271. package/src/persona-website-provider.mjs +134 -0
  272. package/src/persona-zip.mjs +87 -0
  273. package/src/personas.mjs +159 -0
  274. package/src/platform-metadata-analysis.mjs +314 -0
  275. package/src/policy-design-gates.mjs +623 -0
  276. package/src/policy-gate-integration.mjs +318 -0
  277. package/src/portfolio.mjs +509 -0
  278. package/src/power-platform-source-map.mjs +190 -0
  279. package/src/project.mjs +449 -0
  280. package/src/prototype-iterations.mjs +730 -0
  281. package/src/repository-source-map.mjs +603 -0
  282. package/src/runtime/afk-conductor.mjs +973 -0
  283. package/src/runtime/context-assembly.mjs +457 -0
  284. package/src/runtime/context-benchmarks.mjs +115 -0
  285. package/src/runtime/dashboard-actions.mjs +109 -0
  286. package/src/runtime/dashboard-handoffs.mjs +149 -0
  287. package/src/runtime/dashboard-server.mjs +1272 -0
  288. package/src/runtime/dashboard.mjs +197 -0
  289. package/src/runtime/database.mjs +789 -0
  290. package/src/runtime/error-reporting.mjs +581 -0
  291. package/src/runtime/evidence-depth-workspace.mjs +412 -0
  292. package/src/runtime/execution-leases.mjs +299 -0
  293. package/src/runtime/guided-discovery.mjs +350 -0
  294. package/src/runtime/guided-intents.mjs +517 -0
  295. package/src/runtime/impact-analysis.mjs +535 -0
  296. package/src/runtime/intents.mjs +222 -0
  297. package/src/runtime/knowledge.mjs +86 -0
  298. package/src/runtime/lifecycle-hooks.mjs +1239 -0
  299. package/src/runtime/mcp-config.mjs +110 -0
  300. package/src/runtime/mcp-server.mjs +1885 -0
  301. package/src/runtime/palace.mjs +362 -0
  302. package/src/runtime/paths.mjs +58 -0
  303. package/src/runtime/persona-engagement.mjs +255 -0
  304. package/src/runtime/phase-contributions.mjs +594 -0
  305. package/src/runtime/policy-workspace.mjs +170 -0
  306. package/src/runtime/prototype-iterations.mjs +235 -0
  307. package/src/runtime/provider-adapters.mjs +163 -0
  308. package/src/runtime/repository-index.mjs +838 -0
  309. package/src/runtime/runs.mjs +185 -0
  310. package/src/runtime/security-validation.mjs +1230 -0
  311. package/src/runtime/starter-materialisation.mjs +1155 -0
  312. package/src/runtime/team-hub-client.mjs +479 -0
  313. package/src/runtime/team-hub-database.mjs +288 -0
  314. package/src/runtime/team-hub-server.mjs +191 -0
  315. package/src/runtime/team-hub.mjs +110 -0
  316. package/src/runtime/tree-sitter-index.mjs +390 -0
  317. package/src/runtime/version.mjs +1 -0
  318. package/src/runtime/work.mjs +633 -0
  319. package/src/salesforce-source-map.mjs +212 -0
  320. package/src/security-validation-config.mjs +224 -0
  321. package/src/solution-readiness.mjs +620 -0
  322. package/src/starter-materialisation-contract.mjs +407 -0
  323. package/src/task-graph.mjs +544 -0
  324. package/src/team-hub-resources.mjs +239 -0
  325. package/src/team-hub.mjs +242 -0
  326. package/src/test-scenarios.mjs +622 -0
  327. package/src/validation-config.mjs +289 -0
  328. package/templates/SPECS/1.Scope/personas/registry.yaml +12 -0
  329. package/templates/SPECS/5.Strategy/patterns/context-packet.md +119 -0
  330. package/templates/SPECS/6.Build/_tracker-template.md +16 -0
  331. package/templates/SPECS/pipeline.yaml +62 -0
  332. package/templates/discovery-answers.yaml +86 -0
  333. package/templates/intent-body.md +21 -0
  334. package/tests/fixtures/context-benchmarks.json +9 -0
@@ -0,0 +1,594 @@
1
+ import { createHash, randomUUID } from 'node:crypto';
2
+ import { existsSync, mkdirSync, readFileSync, readdirSync, renameSync, rmSync, writeFileSync } from 'node:fs';
3
+ import { dirname, relative, resolve } from 'node:path';
4
+ import { readDeliveryState } from '../delivery.mjs';
5
+ import { atomicJson, isWithin } from '../delivery-documents.mjs';
6
+ import { deriveExecutionState } from '../execution-state.mjs';
7
+ import { loadProjectConfig } from '../project.mjs';
8
+ import { listRuntimeIntents, readRuntimeIntent } from './intents.mjs';
9
+ import { selectContextualPersonas } from './persona-engagement.mjs';
10
+ import { queueDashboardHandoff } from './dashboard-handoffs.mjs';
11
+ import { runtimePaths } from './paths.mjs';
12
+
13
+ const DRAFT_SCHEMA = 'ewai.phase-contribution-draft/v1';
14
+ const RESPONSE_SCHEMA = 'ewai.phase-studio/v1';
15
+ const slugPattern = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
16
+ const digestPattern = /^[a-f0-9]{64}$/;
17
+ const MAX_TEXT = 8_000;
18
+ const MAX_NAME = 160;
19
+ const MAX_ITEMS = 50;
20
+ const MAX_DRAFT_BYTES = 160_000;
21
+ const terminalStatuses = new Set(['completed', 'approved', 'skipped', 'not-supported', 'waived']);
22
+ const contexts = Object.freeze({
23
+ business: {
24
+ id: 'business', title: 'Business-facing owner', ownerKey: 'business',
25
+ responsibility: 'Contribute outcomes, stakeholder needs, trade-offs, acceptance expectations, communications and named business decisions.',
26
+ priorities: ['outcomes', 'stakeholders', 'trade-offs', 'acceptance', 'communications', 'business decisions'],
27
+ questions: ['What outcome must this phase protect?', 'Who accepts the operational consequence?', 'Which trade-off still needs a named decision?'],
28
+ actions: ['save business evidence', 'request technical input', 'record a named decision', 'prepare an owner hand-off'],
29
+ },
30
+ technical: {
31
+ id: 'technical', title: 'Technical owner', ownerKey: 'technical',
32
+ responsibility: 'Contribute repository evidence, constraints, dependencies, standards, architecture, testing, operations and recovery needs.',
33
+ priorities: ['repository evidence', 'constraints', 'dependencies', 'standards', 'architecture', 'testing', 'operations', 'recovery'],
34
+ questions: ['What repository evidence supports this claim?', 'Which dependency or standard constrains the change?', 'How will failure and recovery be tested?'],
35
+ actions: ['save technical evidence', 'identify a conflict', 'request a business decision', 'prepare an owner hand-off'],
36
+ },
37
+ 'shared-review': {
38
+ id: 'shared-review', title: 'Shared review', ownerKey: 'sharedReview',
39
+ responsibility: 'Review both owner perspectives together, preserve disagreement, and record named resolutions or deliberately unresolved items.',
40
+ priorities: ['attribution', 'conflicts', 'decisions', 'unresolved questions', 'limitations', 'acceptance'],
41
+ questions: ['Where do the owner perspectives disagree?', 'Which evidence class and source supports each position?', 'Who owns each unresolved decision?'],
42
+ actions: ['record a shared resolution', 'return an item to a named owner', 'request advisory host review', 'confirm contribution evidence'],
43
+ },
44
+ });
45
+
46
+ const phaseProfiles = Object.freeze({
47
+ reconcile: profile('reconcile', 'Reconcile', [
48
+ topic('observable-behaviour', 'Observable behaviour', 'What does the existing system demonstrably do?'),
49
+ topic('outcomes', 'Protected outcomes', 'Which outcomes must reconciliation preserve?'),
50
+ topic('repository-evidence', 'Repository evidence', 'Which source and history evidence explains the current shape?'),
51
+ topic('constraints', 'Inherited constraints', 'Which constraints are observed, asserted or still uncertain?'),
52
+ topic('decisions', 'Unresolved history', 'Which historical decision needs confirmation?'),
53
+ ], ['reconciliation', 'observable behaviour', 'repository evidence', 'history', 'constraints', 'decisions']),
54
+ plan: profile('plan', 'Plan', [
55
+ topic('outcomes', 'Outcomes', 'What useful result must the implementation protect?'),
56
+ topic('scope', 'Scope and boundaries', 'What is included, excluded and deliberately deferred?'),
57
+ topic('dependencies', 'Dependencies', 'Which business and technical dependencies shape delivery?'),
58
+ topic('trade-offs', 'Trade-offs', 'Which compromise needs visible owner judgement?'),
59
+ topic('acceptance', 'Acceptance', 'What evidence will demonstrate the outcome?'),
60
+ topic('decisions', 'Decisions and questions', 'What remains owned, decided or unresolved?'),
61
+ ], ['planning', 'outcomes', 'dependencies', 'trade-offs', 'acceptance', 'decisions']),
62
+ 'test-plan': profile('test-plan', 'Test Plan', [
63
+ topic('journeys', 'Journeys and scenarios', 'Which happy, alternate, failure and recovery paths matter?'),
64
+ topic('acceptance', 'Evidence oracles', 'What observable result proves or disproves each claim?'),
65
+ topic('environments', 'Environments and data', 'Which environments, devices and data conditions are required?'),
66
+ topic('testing', 'Technical coverage', 'Which automated and manual checks are proportionate?'),
67
+ topic('recovery', 'Failure and recovery', 'How will recovery and degraded behaviour be examined?'),
68
+ topic('decisions', 'Acceptance ownership', 'Who reviews each evidence route?'),
69
+ ], ['test plan', 'journeys', 'acceptance', 'testing', 'environments', 'recovery']),
70
+ 'delivery-preparation': profile('delivery-preparation', 'Delivery preparation', [
71
+ topic('readiness', 'Readiness', 'What is ready and what remains explicitly incomplete?'),
72
+ topic('operations', 'Operations', 'Who operates, supports and observes the change?'),
73
+ topic('communications', 'Communications', 'Who needs to understand the change and when?'),
74
+ topic('recovery', 'Recovery', 'What rollback or recovery route is available?'),
75
+ topic('acceptance', 'Acceptance evidence', 'Which evidence is ready for accountable review?'),
76
+ topic('decisions', 'Residual decisions', 'Which risk, limitation or decision remains named?'),
77
+ ], ['delivery', 'readiness', 'operations', 'communications', 'recovery', 'acceptance']),
78
+ 'manual-qa-preparation': profile('manual-qa-preparation', 'Manual QA preparation', [
79
+ topic('journeys', 'Representative journeys', 'Which participant journeys need human examination?'),
80
+ topic('environments', 'Devices and environments', 'Where must the experience be exercised?'),
81
+ topic('accessibility', 'Accessibility', 'Which keyboard, focus, announcement and responsive behaviours need review?'),
82
+ topic('observations', 'Observations', 'What should the reviewer record rather than infer?'),
83
+ topic('limitations', 'Known limitations', 'Which limitations must remain visible during acceptance?'),
84
+ topic('acceptance', 'Named acceptance', 'Who may accept the Manual QA evidence?'),
85
+ ], ['manual QA', 'journeys', 'accessibility', 'observations', 'limitations', 'acceptance']),
86
+ });
87
+
88
+ const phaseToProfile = Object.freeze({
89
+ reconcile: 'reconcile', plan: 'plan', 'test-plan': 'test-plan', delivery: 'delivery-preparation', 'manual-qa': 'manual-qa-preparation',
90
+ });
91
+ const evidenceClasses = new Set(['repository-fact', 'participant-statement', 'imported-source', 'persona-hypothesis', 'named-decision', 'unresolved-question']);
92
+
93
+ function topic(id, title, question) {
94
+ return { id, title, question };
95
+ }
96
+
97
+ function profile(id, title, topics, signals) {
98
+ return { id, title, topics, signals };
99
+ }
100
+
101
+ function failure(message, statusCode = 409) {
102
+ const error = new Error(message);
103
+ error.statusCode = statusCode;
104
+ return error;
105
+ }
106
+
107
+ function hash(value) {
108
+ return createHash('sha256').update(typeof value === 'string' ? value : JSON.stringify(value)).digest('hex');
109
+ }
110
+
111
+ function cleanText(value, label, maxLength = MAX_TEXT) {
112
+ const text = String(value ?? '').trim();
113
+ if (text.length > maxLength) throw failure(`${label} exceeds ${maxLength} characters`, 400);
114
+ if (/[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f]/.test(text)) throw failure(`${label} contains unsafe control characters`, 400);
115
+ return text;
116
+ }
117
+
118
+ function assertObject(value, label) {
119
+ if (!value || typeof value !== 'object' || Array.isArray(value)) throw failure(`${label} must be an object`, 400);
120
+ return value;
121
+ }
122
+
123
+ function assertKnownFields(value, allowed, label) {
124
+ for (const key of Object.keys(value)) {
125
+ if (!allowed.includes(key)) throw failure(`${label} contains unknown field: ${key}`, 400);
126
+ }
127
+ }
128
+
129
+ function assertSlug(slug) {
130
+ if (!slugPattern.test(slug)) throw failure(`Delivery slug must be lower kebab-case: ${slug}`, 400);
131
+ }
132
+
133
+ function currentPhaseRecord(state, phase) {
134
+ if (phase === 'manual-qa') return (state.humanGates ?? []).find((item) => item.id === phase) ?? null;
135
+ return [...(state.phases ?? []), ...(state.adjuncts ?? [])].find((item) => item.id === phase) ?? null;
136
+ }
137
+
138
+ function resolveGovernedContext(projectRoot, slug, options = {}) {
139
+ assertSlug(slug);
140
+ const intent = options.intent ?? readRuntimeIntent(projectRoot, slug);
141
+ if (!intent || intent.slug !== slug) throw failure(`Work item not found: ${slug}`, 404);
142
+ const state = options.deliveryState ?? readDeliveryState(projectRoot, slug);
143
+ const catalog = new Map((options.intentCatalogue ?? listRuntimeIntents(projectRoot)).map((item) => [item.id, item]));
144
+ const execution = options.executionState ?? deriveExecutionState(projectRoot, intent, { intentCatalog: catalog });
145
+ const phase = String(state.currentPhase ?? execution.lifecycle?.nextPhase ?? '').trim();
146
+ const profileId = phaseToProfile[phase] ?? null;
147
+ const phaseRecord = currentPhaseRecord(state, phase);
148
+ const profileValue = profileId ? phaseProfiles[profileId] : null;
149
+ const sourceDigest = hash({
150
+ intent: {
151
+ id: intent.id, status: intent.status, deliveryStatus: intent.deliveryStatus, currentPhase: intent.currentPhase,
152
+ contentDigest: hash(intent.markdown ?? { title: intent.title, relationships: intent.relationships, personas: intent.personas }),
153
+ },
154
+ delivery: { schema: state.schema, slug: state.slug, status: state.status, currentPhase: state.currentPhase, updatedAt: state.updatedAt, phaseStatus: phaseRecord?.status ?? null },
155
+ execution: { valid: execution.valid, lifecycle: execution.lifecycle, blockers: execution.blockers ?? [] },
156
+ });
157
+ const profileDigest = profileValue ? hash(profileValue) : null;
158
+ const blockers = [...(execution.blockers ?? [])];
159
+ if (profileValue && (!phaseRecord || terminalStatuses.has(phaseRecord.status) || phaseRecord.status !== 'running')) {
160
+ blockers.push({ code: 'phase-not-contributable', message: `Phase ${phase} is ${phaseRecord?.status ?? 'unavailable'} and is read-only.`, evidence: phase });
161
+ }
162
+ const supported = Boolean(profileValue);
163
+ const contributable = supported && execution.valid === true && blockers.length === 0;
164
+ return { projectRoot: resolve(projectRoot), intent, state, execution, phase, phaseRecord, profile: profileValue, profileDigest, sourceDigest, blockers, supported, contributable };
165
+ }
166
+
167
+ function draftPath(projectRoot, slug, profileId) {
168
+ return runtimePaths(projectRoot).phaseContributionDraftPath(slug, profileId);
169
+ }
170
+
171
+ function findOtherDraft(projectRoot, slug, profileId) {
172
+ const folder = resolve(runtimePaths(projectRoot).phaseContributionsRoot, slug);
173
+ if (!existsSync(folder)) return null;
174
+ const candidate = readdirSync(folder).find((name) => name.endsWith('.json') && name !== `${profileId}.json`);
175
+ if (!candidate) return null;
176
+ try { return JSON.parse(readFileSync(resolve(folder, candidate), 'utf8')); }
177
+ catch { return { phase: candidate.replace(/\.json$/, '') }; }
178
+ }
179
+
180
+ function draftDigest(draft) {
181
+ const { digest: _digest, ...content } = draft;
182
+ return hash(content);
183
+ }
184
+
185
+ function initialDraft(context, now) {
186
+ return {
187
+ schema: DRAFT_SCHEMA,
188
+ slug: context.intent.slug,
189
+ intentId: context.intent.id,
190
+ phase: context.phase,
191
+ profileId: context.profile.id,
192
+ profileDigest: context.profileDigest,
193
+ sourceDigest: context.sourceDigest,
194
+ deliveryUpdatedAt: context.state.updatedAt ?? null,
195
+ revision: 0,
196
+ digest: '',
197
+ ownerContext: 'business',
198
+ owners: { business: '', technical: '', sharedReview: '' },
199
+ entries: [],
200
+ openQuestions: [],
201
+ conflicts: [],
202
+ limitations: [],
203
+ handoffs: [],
204
+ confirmations: [],
205
+ activePersonaIds: [],
206
+ createdAt: now,
207
+ updatedAt: now,
208
+ };
209
+ }
210
+
211
+ function readDraft(projectRoot, context, options = {}) {
212
+ const path = draftPath(projectRoot, context.intent.slug, context.profile.id);
213
+ if (!existsSync(path)) return initialDraft(context, options.now ?? new Date().toISOString());
214
+ let draft;
215
+ try { draft = JSON.parse(readFileSync(path, 'utf8')); }
216
+ catch (error) { throw failure(`Phase contribution draft is not valid JSON: ${error.message}`); }
217
+ if (draft.schema !== DRAFT_SCHEMA || draft.slug !== context.intent.slug || draft.profileId !== context.profile.id || !Number.isInteger(draft.revision) || draft.revision < 1) {
218
+ throw failure('Phase contribution draft has an unsupported schema, delivery, profile or revision');
219
+ }
220
+ if (draft.phase !== context.phase) throw failure(`The saved contribution belongs to ${draft.phase}; the current phase changed to ${context.phase}.`);
221
+ if (draft.profileDigest !== context.profileDigest) throw failure('The server-owned phase contribution profile changed; review before continuing.');
222
+ if (draft.sourceDigest !== context.sourceDigest) throw failure('The governed delivery source changed; review the current phase before continuing.');
223
+ if (draft.digest !== draftDigest(draft)) throw failure('The phase contribution draft digest is invalid.');
224
+ return draft;
225
+ }
226
+
227
+ function readDraftForMutation(projectRoot, context, options) {
228
+ const currentPath = draftPath(projectRoot, context.intent.slug, context.profile.id);
229
+ if (!existsSync(currentPath)) {
230
+ const other = findOtherDraft(projectRoot, context.intent.slug, context.profile.id);
231
+ if (other) throw failure(`The saved contribution belongs to ${other.phase ?? other.profileId}; the current phase changed to ${context.phase}.`);
232
+ }
233
+ return readDraft(projectRoot, context, options);
234
+ }
235
+
236
+ function availability(catalogue) {
237
+ const result = {};
238
+ for (const tier of ['core', 'project', 'premium', 'personal']) {
239
+ const count = catalogue.filter((persona) => persona.tier === tier).length;
240
+ result[tier] = {
241
+ installed: count > 0,
242
+ count,
243
+ reason: count ? `${count} installed ${tier} persona${count === 1 ? '' : 's'} available.` : `No installed ${tier} personas were found; this does not block Phase Studio.`,
244
+ };
245
+ }
246
+ return result;
247
+ }
248
+
249
+ function activePersonas(context, draft, catalogue) {
250
+ const projection = contexts[draft.ownerContext];
251
+ return selectContextualPersonas({
252
+ contextLabel: `${context.profile.title} ${projection.title} context`,
253
+ signals: [...context.profile.signals, ...projection.priorities],
254
+ context: { entries: draft.entries.map(({ topic, classification, statement }) => ({ topic, classification, statement })), questions: draft.openQuestions, conflicts: draft.conflicts },
255
+ personaCatalogue: catalogue,
256
+ limit: 4,
257
+ });
258
+ }
259
+
260
+ function personaChanges(previousIds, personas) {
261
+ const currentIds = personas.map(({ id }) => id);
262
+ return {
263
+ joined: personas.filter(({ id }) => !previousIds.includes(id)).map(({ id, name, tier }) => ({ id, name, tier })),
264
+ left: previousIds.filter((id) => !currentIds.includes(id)),
265
+ };
266
+ }
267
+
268
+ function safeDraft(draft) {
269
+ return structuredClone(draft);
270
+ }
271
+
272
+ function response(context, draft, options = {}) {
273
+ const catalogue = Array.isArray(options.personas) ? options.personas : [];
274
+ const personas = context.profile ? activePersonas(context, draft, catalogue) : [];
275
+ const previousIds = options.previousPersonaIds ?? draft.activePersonaIds ?? [];
276
+ const isSaved = draft.revision > 0;
277
+ return {
278
+ schema: RESPONSE_SCHEMA,
279
+ status: !context.supported ? 'unsupported' : context.blockers.length ? 'blocked' : isSaved ? 'draft' : 'empty',
280
+ intent: { id: context.intent.id, slug: context.intent.slug, title: context.intent.title },
281
+ phase: { id: context.phase, status: context.phaseRecord?.status ?? 'unknown', sourceDigest: context.sourceDigest },
282
+ integrity: { valid: context.execution.valid === true, blockers: context.execution.blockers ?? [] },
283
+ blockers: context.blockers,
284
+ profile: context.profile ? structuredClone(context.profile) : null,
285
+ profileDigest: context.profileDigest,
286
+ contexts: structuredClone(contexts),
287
+ context: structuredClone(contexts[draft.ownerContext] ?? contexts.business),
288
+ draft: safeDraft(draft),
289
+ activePersonas: personas,
290
+ personaChanges: personaChanges(previousIds, personas),
291
+ personaAvailability: availability(catalogue),
292
+ guidance: {
293
+ baselineComplete: true,
294
+ baselineQuestions: context.profile ? [...context.profile.topics.map((item) => item.question), ...contexts[draft.ownerContext].questions] : [],
295
+ advisoryNotice: 'Standard host-model reasoning plus installed core and project personas is complete. Personas are advisory lenses, not participant evidence, validation, acceptance or approval.',
296
+ premiumOptional: true,
297
+ },
298
+ capabilities: {
299
+ contribute: context.contributable,
300
+ saveDraft: context.contributable,
301
+ discardDraft: context.contributable && isSaved,
302
+ handoff: context.contributable && isSaved,
303
+ requestReview: context.contributable && isSaved,
304
+ confirmContribution: context.contributable && isSaved,
305
+ },
306
+ destinations: context.profile ? contributionDestination(context, draft) : [],
307
+ authority: {
308
+ delivery: 'none', ownerContextIsAuthentication: false, personasAreEvidence: false,
309
+ phaseCompleted: false, buildApproved: false, manualQaApproved: false, releaseApproved: false,
310
+ },
311
+ };
312
+ }
313
+
314
+ function contributionDestination(context, draft) {
315
+ const { paths } = loadProjectConfig(context.projectRoot);
316
+ const root = relative(paths.projectRoot, resolve(paths.specsRoot, '6.Build', context.intent.slug, 'phase-contributions', context.profile.id)).replaceAll('\\', '/');
317
+ return [`${root}/contribution-<timestamp>-r${draft.revision || '<revision>'}-<digest>.md`, `${root}/contribution-<timestamp>-r${draft.revision || '<revision>'}-<digest>.json`];
318
+ }
319
+
320
+ function normaliseEntry(value, index, context, ownerContext, ownerName, timestamp) {
321
+ const input = assertObject(value, `Evidence entry ${index + 1}`);
322
+ assertKnownFields(input, ['topic', 'classification', 'statement', 'source'], `Evidence entry ${index + 1}`);
323
+ const topicId = cleanText(input.topic, `Evidence entry ${index + 1} topic`, 120);
324
+ if (!context.profile.topics.some((candidate) => candidate.id === topicId)) throw failure(`Unknown ${context.profile.title} evidence topic: ${topicId}`, 400);
325
+ const classification = cleanText(input.classification, `Evidence entry ${index + 1} classification`, 80);
326
+ if (!evidenceClasses.has(classification)) throw failure(`Unsupported evidence classification: ${classification}`, 400);
327
+ const statement = cleanText(input.statement, `Evidence entry ${index + 1} statement`);
328
+ if (!statement) throw failure(`Evidence entry ${index + 1} requires a statement`, 400);
329
+ return {
330
+ id: randomUUID(), topic: topicId, classification, statement,
331
+ source: cleanText(input.source, `Evidence entry ${index + 1} source`, 1_000),
332
+ ownerContext, contributedBy: ownerName, recordedAt: timestamp,
333
+ };
334
+ }
335
+
336
+ function boundedArray(value, label) {
337
+ if (value == null) return [];
338
+ if (!Array.isArray(value)) throw failure(`${label} must be an array`, 400);
339
+ if (value.length > MAX_ITEMS) throw failure(`${label} exceeds ${MAX_ITEMS} items`, 400);
340
+ return value;
341
+ }
342
+
343
+ function persistDraft(projectRoot, context, draft, options = {}) {
344
+ const catalogue = Array.isArray(options.personas) ? options.personas : [];
345
+ draft.activePersonaIds = activePersonas(context, draft, catalogue).map(({ id }) => id);
346
+ draft.digest = draftDigest(draft);
347
+ if (Buffer.byteLength(JSON.stringify(draft), 'utf8') > MAX_DRAFT_BYTES) throw failure(`Phase contribution draft exceeds ${MAX_DRAFT_BYTES} bytes`, 413);
348
+ atomicJson(draftPath(projectRoot, context.intent.slug, context.profile.id), draft);
349
+ return draft;
350
+ }
351
+
352
+ function assertContributable(context) {
353
+ if (!context.supported) throw failure(`Phase ${context.phase || '<unknown>'} does not support Phase Studio contributions.`);
354
+ if (!context.contributable) throw failure(context.blockers[0]?.message ?? 'The current delivery state is read-only.');
355
+ }
356
+
357
+ function assertExpectedRevision(input, draft) {
358
+ const revision = Number(input.expectedRevision);
359
+ if (!Number.isInteger(revision) || revision !== draft.revision) throw failure(`A newer revision of this phase contribution draft exists (current revision ${draft.revision}).`);
360
+ }
361
+
362
+ function normaliseSaveInput(input) {
363
+ const value = assertObject(input, 'Phase contribution request');
364
+ assertKnownFields(value, ['expectedRevision', 'ownerContext', 'ownerName', 'entries', 'questions', 'conflicts', 'resolutions', 'limitations'], 'Phase contribution request');
365
+ return value;
366
+ }
367
+
368
+ export function phaseContributionProfiles() {
369
+ return structuredClone(phaseProfiles);
370
+ }
371
+
372
+ export function readPhaseStudio(projectRoot, slug, options = {}) {
373
+ const context = resolveGovernedContext(projectRoot, slug, options);
374
+ if (!context.profile) {
375
+ const draft = { ...initialDraft({ ...context, profile: { id: 'unsupported' } }, options.now ?? new Date().toISOString()), phase: context.phase, profileId: null, profileDigest: null, sourceDigest: context.sourceDigest };
376
+ draft.digest = draftDigest(draft);
377
+ return response(context, draft, options);
378
+ }
379
+ const draft = readDraft(projectRoot, context, options);
380
+ return response(context, draft, options);
381
+ }
382
+
383
+ export function savePhaseContributionDraft(projectRoot, slug, input = {}, options = {}) {
384
+ const value = normaliseSaveInput(input);
385
+ const context = resolveGovernedContext(projectRoot, slug, options);
386
+ assertContributable(context);
387
+ const current = readDraftForMutation(projectRoot, context, options);
388
+ assertExpectedRevision(value, current);
389
+ const ownerContext = cleanText(value.ownerContext || current.ownerContext, 'Owner context', 40);
390
+ if (!contexts[ownerContext]) throw failure(`Owner context must be one of: ${Object.keys(contexts).join(', ')}`, 400);
391
+ const ownerName = cleanText(value.ownerName || current.owners[contexts[ownerContext].ownerKey], 'Owner name', MAX_NAME);
392
+ if (!ownerName) throw failure('Saving a phase contribution requires a real named owner.', 400);
393
+ const timestamp = options.now ?? new Date().toISOString();
394
+ const draft = structuredClone(current);
395
+ const previousPersonaIds = [...(current.activePersonaIds ?? [])];
396
+ draft.revision += 1;
397
+ draft.ownerContext = ownerContext;
398
+ draft.owners[contexts[ownerContext].ownerKey] = ownerName;
399
+ draft.entries.push(...boundedArray(value.entries, 'Evidence entries').map((entry, index) => normaliseEntry(entry, index, context, ownerContext, ownerName, timestamp)));
400
+ draft.openQuestions.push(...boundedArray(value.questions, 'Open questions').map((question, index) => ({
401
+ id: randomUUID(), question: cleanText(question, `Open question ${index + 1}`), ownerContext, raisedBy: ownerName, status: 'open', recordedAt: timestamp,
402
+ })).filter(({ question }) => question));
403
+ draft.conflicts.push(...boundedArray(value.conflicts, 'Conflicts').map((conflict, index) => {
404
+ const item = assertObject(conflict, `Conflict ${index + 1}`);
405
+ assertKnownFields(item, ['summary', 'businessPosition', 'technicalPosition'], `Conflict ${index + 1}`);
406
+ const summary = cleanText(item.summary, `Conflict ${index + 1} summary`);
407
+ if (!summary) throw failure(`Conflict ${index + 1} requires a summary`, 400);
408
+ return {
409
+ id: randomUUID(), summary, businessPosition: cleanText(item.businessPosition, `Conflict ${index + 1} business position`),
410
+ technicalPosition: cleanText(item.technicalPosition, `Conflict ${index + 1} technical position`), ownerContext, raisedBy: ownerName,
411
+ status: 'unresolved', resolution: null, recordedAt: timestamp,
412
+ };
413
+ }));
414
+ for (const [index, resolution] of boundedArray(value.resolutions, 'Conflict resolutions').entries()) {
415
+ const item = assertObject(resolution, `Conflict resolution ${index + 1}`);
416
+ assertKnownFields(item, ['conflictId', 'resolution'], `Conflict resolution ${index + 1}`);
417
+ const conflict = draft.conflicts.find((candidate) => candidate.id === cleanText(item.conflictId, `Conflict resolution ${index + 1} id`, 80));
418
+ if (!conflict) throw failure(`Unknown conflict for resolution ${index + 1}`, 400);
419
+ const resolutionText = cleanText(item.resolution, `Conflict resolution ${index + 1}`);
420
+ if (!resolutionText) throw failure(`Conflict resolution ${index + 1} requires a resolution`, 400);
421
+ conflict.status = 'resolved';
422
+ conflict.resolution = { statement: resolutionText, resolvedBy: ownerName, ownerContext, recordedAt: timestamp };
423
+ }
424
+ draft.limitations.push(...boundedArray(value.limitations, 'Limitations').map((limitation, index) => ({
425
+ id: randomUUID(), statement: cleanText(limitation, `Limitation ${index + 1}`), recordedBy: ownerName, ownerContext, recordedAt: timestamp,
426
+ })).filter(({ statement }) => statement));
427
+ draft.updatedAt = timestamp;
428
+ persistDraft(projectRoot, context, draft, options);
429
+ return response(context, draft, { ...options, previousPersonaIds });
430
+ }
431
+
432
+ export function discardPhaseContributionDraft(projectRoot, slug, input = {}, options = {}) {
433
+ const value = assertObject(input, 'Phase contribution discard request');
434
+ assertKnownFields(value, ['expectedRevision', 'confirmed'], 'Phase contribution discard request');
435
+ if (value.confirmed !== true) throw failure('Discarding a phase contribution draft requires explicit confirmation.');
436
+ const context = resolveGovernedContext(projectRoot, slug, options);
437
+ assertContributable(context);
438
+ const draft = readDraftForMutation(projectRoot, context, options);
439
+ assertExpectedRevision(value, draft);
440
+ rmSync(draftPath(projectRoot, slug, context.profile.id), { force: true });
441
+ return { schema: 'ewai.phase-contribution-discard/v1', status: 'discarded', revision: draft.revision, authority: { delivery: 'none', evidenceDeleted: false } };
442
+ }
443
+
444
+ export function handoffPhaseContribution(projectRoot, slug, input = {}, options = {}) {
445
+ const value = assertObject(input, 'Phase contribution hand-off request');
446
+ assertKnownFields(value, ['expectedRevision', 'currentDigest', 'destinationContext', 'toOwner', 'reason', 'questions'], 'Phase contribution hand-off request');
447
+ const context = resolveGovernedContext(projectRoot, slug, options);
448
+ assertContributable(context);
449
+ const current = readDraftForMutation(projectRoot, context, options);
450
+ assertExpectedRevision(value, current);
451
+ const currentDigest = cleanText(value.currentDigest, 'Current contribution digest', 64);
452
+ if (!digestPattern.test(currentDigest) || currentDigest !== current.digest) throw failure('The current contribution digest does not match the saved draft.');
453
+ const destinationContext = cleanText(value.destinationContext, 'Destination context', 40);
454
+ if (!contexts[destinationContext]) throw failure(`Destination context must be one of: ${Object.keys(contexts).join(', ')}`, 400);
455
+ if (destinationContext === current.ownerContext) throw failure('An owner hand-off requires a distinct destination context.', 400);
456
+ const sourceOwner = current.owners[contexts[current.ownerContext].ownerKey];
457
+ if (!sourceOwner) throw failure('An owner hand-off requires a named source owner.', 400);
458
+ const toOwner = cleanText(value.toOwner, 'Destination owner', MAX_NAME);
459
+ if (!toOwner) throw failure('An owner hand-off requires a named destination owner.', 400);
460
+ const reason = cleanText(value.reason, 'Hand-off reason');
461
+ if (!reason) throw failure('An owner hand-off requires a reason.', 400);
462
+ const questions = boundedArray(value.questions, 'Hand-off questions').map((question, index) => cleanText(question, `Hand-off question ${index + 1}`)).filter(Boolean);
463
+ if (!questions.length) throw failure('An owner hand-off requires at least one next-owner question.', 400);
464
+ const timestamp = options.now ?? new Date().toISOString();
465
+ const draft = structuredClone(current);
466
+ const previousPersonaIds = [...(current.activePersonaIds ?? [])];
467
+ draft.revision += 1;
468
+ draft.handoffs.push({
469
+ id: randomUUID(), fromContext: current.ownerContext, fromOwner: sourceOwner,
470
+ toContext: destinationContext, toOwner, reason, questions,
471
+ sourceRevision: current.revision, sourceDigest: current.digest, handedOffAt: timestamp,
472
+ });
473
+ draft.ownerContext = destinationContext;
474
+ draft.owners[contexts[destinationContext].ownerKey] = toOwner;
475
+ draft.updatedAt = timestamp;
476
+ persistDraft(projectRoot, context, draft, options);
477
+ return response(context, draft, { ...options, previousPersonaIds });
478
+ }
479
+
480
+ export function requestPhaseContributionReview(projectRoot, slug, input = {}, options = {}) {
481
+ const value = assertObject(input, 'Phase contribution review request');
482
+ assertKnownFields(value, ['expectedRevision', 'currentDigest'], 'Phase contribution review request');
483
+ const context = resolveGovernedContext(projectRoot, slug, options);
484
+ assertContributable(context);
485
+ const draft = readDraftForMutation(projectRoot, context, options);
486
+ assertExpectedRevision(value, draft);
487
+ const currentDigest = cleanText(value.currentDigest, 'Current contribution digest', 64);
488
+ if (!digestPattern.test(currentDigest) || currentDigest !== draft.digest) throw failure('The current contribution digest does not match the saved draft.');
489
+ const personas = activePersonas(context, draft, Array.isArray(options.personas) ? options.personas : [])
490
+ .map(({ id, name, tier, matchedSignals, engagementReason }) => ({ id, name, tier, matchedSignals, engagementReason }));
491
+ const topicNames = [...new Set(draft.entries.map(({ topic: topicId }) => topicId))];
492
+ const unresolvedConflicts = draft.conflicts.filter(({ status }) => status !== 'resolved').length;
493
+ const summary = `${draft.entries.length} attributed entr${draft.entries.length === 1 ? 'y' : 'ies'}${topicNames.length ? ` across ${topicNames.join(', ')}` : ''}; ${draft.openQuestions.filter(({ status }) => status !== 'resolved').length} open question(s); ${unresolvedConflicts} unresolved conflict(s); ${draft.limitations.length} recorded limitation(s).`;
494
+ const queueReview = options.queueReview ?? queueDashboardHandoff;
495
+ const handoff = queueReview(projectRoot, {
496
+ action: 'review-contribution', intentId: context.intent.id, slug: context.intent.slug, title: context.intent.title,
497
+ phase: context.profile.id, authority: 'none',
498
+ reviewContext: { revision: draft.revision, digest: draft.digest, ownerContext: draft.ownerContext, summary, activePersonas: personas },
499
+ });
500
+ return {
501
+ schema: 'ewai.phase-contribution-review/v1', status: handoff.status, handoff,
502
+ authority: 'none', draftRevision: draft.revision, draftDigest: draft.digest,
503
+ notice: 'This hand-off requests advisory challenge only. It cannot change delivery state, confirm evidence or approve work.',
504
+ };
505
+ }
506
+
507
+ function relativePath(projectRoot, path) {
508
+ return relative(resolve(projectRoot), path).replaceAll('\\', '/');
509
+ }
510
+
511
+ function uniqueBundlePaths(projectRoot, context, draft, timestamp) {
512
+ const { paths } = loadProjectConfig(projectRoot);
513
+ const root = resolve(paths.specsRoot, '6.Build', context.intent.slug, 'phase-contributions', context.profile.id);
514
+ if (!isWithin(paths.specsRoot, root)) throw failure('Unsafe contribution evidence destination.');
515
+ const compactTime = timestamp.replace(/[^0-9]/g, '').slice(0, 14) || 'undated';
516
+ const base = `contribution-${compactTime}-r${draft.revision}-${draft.digest.slice(0, 8)}`;
517
+ let stem = base;
518
+ while (existsSync(resolve(root, `${stem}.json`)) || existsSync(resolve(root, `${stem}.md`))) stem = `${base}-${randomUUID().slice(0, 8)}`;
519
+ return { root, markdown: resolve(root, `${stem}.md`), json: resolve(root, `${stem}.json`) };
520
+ }
521
+
522
+ function line(value) {
523
+ return String(value ?? '').replace(/\r?\n/g, ' ').trim();
524
+ }
525
+
526
+ function renderBundleMarkdown(bundle) {
527
+ const evidence = bundle.evidence.length
528
+ ? bundle.evidence.map((item) => `- **${line(item.classification)} · ${line(item.topic)} · ${line(item.contributedBy)} (${line(item.ownerContext)}):** ${line(item.statement)}${item.source ? ` — Source: ${line(item.source)}` : ''}`).join('\n')
529
+ : '- None recorded.';
530
+ const questions = bundle.openQuestions.length ? bundle.openQuestions.map((item) => `- **${line(item.raisedBy)}:** ${line(item.question)} (${line(item.status)})`).join('\n') : '- None recorded.';
531
+ const conflicts = bundle.conflicts.length ? bundle.conflicts.map((item) => `- **${line(item.summary)}** — ${line(item.status)}${item.resolution ? `; ${line(item.resolution.statement)} (${line(item.resolution.resolvedBy)})` : ''}`).join('\n') : '- None recorded.';
532
+ return `# Phase contribution: ${line(bundle.intent.title)}\n\n**Phase:** ${line(bundle.phase.title)} (\`${line(bundle.phase.id)}\`)\n\n**Confirmed by:** ${line(bundle.confirmation.confirmedBy)} at ${line(bundle.confirmation.confirmedAt)}\n\n**Draft revision:** ${bundle.confirmation.revision} · \`${bundle.confirmation.digest}\`\n\n> ${bundle.assurance.notice}\n\n## Named owners\n\n- Business-facing: ${line(bundle.owners.business) || 'Not named'}\n- Technical: ${line(bundle.owners.technical) || 'Not named'}\n- Shared review: ${line(bundle.owners.sharedReview) || 'Not named'}\n\n## Attributed evidence\n\n${evidence}\n\n## Open questions\n\n${questions}\n\n## Conflicts and resolutions\n\n${conflicts}\n\n## Limitations\n\n${bundle.limitations.length ? bundle.limitations.map((item) => `- ${line(item.statement)} — ${line(item.recordedBy)} (${line(item.ownerContext)})`).join('\n') : '- None recorded.'}\n\n## Owner hand-offs\n\n${bundle.handoffs.length ? bundle.handoffs.map((item) => `- ${line(item.fromOwner)} (${line(item.fromContext)}) → ${line(item.toOwner)} (${line(item.toContext)}), revision ${item.sourceRevision}, digest \`${item.sourceDigest}\`: ${line(item.reason)}`).join('\n') : '- None recorded.'}\n\n## Authority boundary\n\nThis bundle does not complete a phase, approve Build, accept Manual QA, dispose of security findings, deploy, certify or release work.\n`;
533
+ }
534
+
535
+ function writePair(paths, markdown, bundle) {
536
+ mkdirSync(paths.root, { recursive: true });
537
+ const markdownTemporary = `${paths.markdown}.${process.pid}.${randomUUID()}.tmp`;
538
+ const jsonTemporary = `${paths.json}.${process.pid}.${randomUUID()}.tmp`;
539
+ try {
540
+ writeFileSync(markdownTemporary, markdown, 'utf8');
541
+ writeFileSync(jsonTemporary, `${JSON.stringify(bundle, null, 2)}\n`, 'utf8');
542
+ renameSync(markdownTemporary, paths.markdown);
543
+ try { renameSync(jsonTemporary, paths.json); }
544
+ catch (error) { rmSync(paths.markdown, { force: true }); throw error; }
545
+ } finally {
546
+ rmSync(markdownTemporary, { force: true });
547
+ rmSync(jsonTemporary, { force: true });
548
+ }
549
+ }
550
+
551
+ export function confirmPhaseContribution(projectRoot, slug, input = {}, options = {}) {
552
+ const value = assertObject(input, 'Phase contribution confirmation request');
553
+ assertKnownFields(value, ['expectedRevision', 'currentDigest', 'confirmed', 'confirmedBy'], 'Phase contribution confirmation request');
554
+ if (value.confirmed !== true) throw failure('Contribution evidence materialisation requires explicit confirmation.');
555
+ const confirmedBy = cleanText(value.confirmedBy, 'Confirmer name', MAX_NAME);
556
+ if (!confirmedBy) throw failure('Contribution evidence materialisation requires an accountable confirmer name.', 400);
557
+ const context = resolveGovernedContext(projectRoot, slug, options);
558
+ assertContributable(context);
559
+ const current = readDraftForMutation(projectRoot, context, options);
560
+ assertExpectedRevision(value, current);
561
+ if (!current.entries.length) throw failure('Contribution confirmation requires at least one attributed evidence entry.');
562
+ const currentDigest = cleanText(value.currentDigest, 'Current contribution digest', 64);
563
+ if (!digestPattern.test(currentDigest) || currentDigest !== current.digest) throw failure('The current contribution digest does not match the saved draft.');
564
+ const timestamp = options.now ?? new Date().toISOString();
565
+ const paths = uniqueBundlePaths(projectRoot, context, current, timestamp);
566
+ const created = { markdownPath: relativePath(projectRoot, paths.markdown), jsonPath: relativePath(projectRoot, paths.json) };
567
+ const bundle = {
568
+ schema: 'ewai.phase-contribution/v1',
569
+ intent: { id: context.intent.id, slug: context.intent.slug, title: context.intent.title },
570
+ phase: { id: context.profile.id, title: context.profile.title, governedPhase: context.phase, deliveryUpdatedAt: current.deliveryUpdatedAt, sourceDigest: current.sourceDigest, profileDigest: current.profileDigest },
571
+ confirmation: { confirmedBy, confirmedAt: timestamp, revision: current.revision, digest: current.digest },
572
+ owners: structuredClone(current.owners),
573
+ evidence: structuredClone(current.entries), openQuestions: structuredClone(current.openQuestions),
574
+ conflicts: structuredClone(current.conflicts), limitations: structuredClone(current.limitations), handoffs: structuredClone(current.handoffs),
575
+ activePersonas: activePersonas(context, current, Array.isArray(options.personas) ? options.personas : []).map(({ id, name, tier, matchedSignals, engagementReason }) => ({ id, name, tier, matchedSignals, engagementReason })),
576
+ assurance: {
577
+ notice: 'This is supporting evidence only. It is not a phase artefact, gate decision, validation, acceptance, approval, certification or release authority.',
578
+ personasAreAdvisory: true, deliveryAuthority: 'none', deliberatelyUnresolved: current.openQuestions.filter(({ status }) => status !== 'resolved').length + current.conflicts.filter(({ status }) => status !== 'resolved').length,
579
+ },
580
+ created,
581
+ };
582
+ writePair(paths, renderBundleMarkdown(bundle), bundle);
583
+
584
+ const draft = structuredClone(current);
585
+ draft.revision += 1;
586
+ draft.confirmations.push({ confirmedBy, confirmedAt: timestamp, sourceRevision: current.revision, sourceDigest: current.digest, ...created });
587
+ draft.updatedAt = timestamp;
588
+ persistDraft(projectRoot, context, draft, options);
589
+ return {
590
+ schema: 'ewai.phase-contribution-confirmation/v1', status: 'confirmed', confirmation: bundle.confirmation, created,
591
+ draftRevision: draft.revision, draftDigest: draft.digest,
592
+ authority: { phaseCompleted: false, buildApproved: false, manualQaApproved: false, securityDispositionRecorded: false, releaseApproved: false },
593
+ };
594
+ }