@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,730 @@
1
+ import { existsSync, lstatSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from 'node:fs';
2
+ import { basename, isAbsolute, relative, resolve } from 'node:path';
3
+ import { projectPaths } from './paths.mjs';
4
+ import { sha256 } from './delivery-documents.mjs';
5
+
6
+ export const PROTOTYPE_REVIEW_DISPOSITIONS = Object.freeze([
7
+ 'incorporate',
8
+ 'incorporate-with-modification',
9
+ 'defer',
10
+ 'reject',
11
+ 'escalate',
12
+ ]);
13
+
14
+ export const PROTOTYPE_EVIDENCE_CHANNELS = Object.freeze([
15
+ 'source',
16
+ 'rendered-viewport',
17
+ 'interaction',
18
+ 'assistive-technology',
19
+ 'user-research',
20
+ 'manual-qa',
21
+ 'release',
22
+ ]);
23
+
24
+ const tiers = new Set(['project', 'premium', 'personal', 'core']);
25
+ const severities = new Set(['advisory', 'material', 'critical']);
26
+ const dispositions = new Set(PROTOTYPE_REVIEW_DISPOSITIONS);
27
+ const safeTokenPattern = /^[a-z0-9][a-z0-9._:@/-]{0,199}$/i;
28
+ const digestPattern = /^sha256:[a-f0-9]{64}$/;
29
+
30
+ function fail(message) {
31
+ throw new Error(`Prototype iteration: ${message}`);
32
+ }
33
+
34
+ function object(value, label) {
35
+ if (!value || typeof value !== 'object' || Array.isArray(value)) fail(`${label} must be an object`);
36
+ return value;
37
+ }
38
+
39
+ function knownFields(value, allowed, label) {
40
+ for (const key of Object.keys(value)) if (!allowed.includes(key)) fail(`${label} has unknown field ${key}`);
41
+ }
42
+
43
+ function text(value, label, maximum = 200, options = {}) {
44
+ const cleaned = String(value ?? '').trim();
45
+ if (!cleaned && !options.optional) fail(`${label} is required`);
46
+ if (cleaned.length > maximum) fail(`${label} exceeds ${maximum} characters`);
47
+ return cleaned;
48
+ }
49
+
50
+ function token(value, label, options = {}) {
51
+ const cleaned = text(value, label, options.maximum ?? 200, options);
52
+ if (cleaned && (!safeTokenPattern.test(cleaned) || cleaned.includes('..') || cleaned.startsWith('/'))) {
53
+ fail(`${label} must be a bounded safe identifier`);
54
+ }
55
+ return cleaned;
56
+ }
57
+
58
+ function digest(value, label, options = {}) {
59
+ const cleaned = text(value, label, 80, options).toLowerCase();
60
+ if (cleaned && !digestPattern.test(cleaned)) fail(`${label} must be a SHA-256 digest`);
61
+ return cleaned;
62
+ }
63
+
64
+ function list(value, label, maximum = 100) {
65
+ if (!Array.isArray(value)) fail(`${label} must be an array`);
66
+ if (value.length > maximum) fail(`${label} exceeds ${maximum} entries`);
67
+ return value;
68
+ }
69
+
70
+ function uniqueTokens(value, label, maximum = 100) {
71
+ const result = list(value ?? [], label, maximum).map((entry, index) => token(entry, `${label} ${index + 1}`));
72
+ if (new Set(result).size !== result.length) fail(`${label} contains duplicate values`);
73
+ return result.sort((left, right) => left.localeCompare(right));
74
+ }
75
+
76
+ function uniqueTexts(value, label, maximum = 100, textMaximum = 500) {
77
+ const result = list(value ?? [], label, maximum).map((entry, index) => text(entry, `${label} ${index + 1}`, textMaximum));
78
+ if (new Set(result).size !== result.length) fail(`${label} contains duplicate values`);
79
+ return result.sort((left, right) => left.localeCompare(right));
80
+ }
81
+
82
+ function canonical(value) {
83
+ if (Array.isArray(value)) return value.map(canonical);
84
+ if (!value || typeof value !== 'object') return value;
85
+ return Object.fromEntries(Object.keys(value).sort().map((key) => [key, canonical(value[key])]));
86
+ }
87
+
88
+ function hash(value) {
89
+ return `sha256:${sha256(JSON.stringify(canonical(value)))}`;
90
+ }
91
+
92
+ function boundedCount(value, label) {
93
+ const parsed = Number(value ?? 0);
94
+ if (!Number.isInteger(parsed) || parsed < 0 || parsed > 10_000) fail(`${label} must be an integer from 0 to 10000`);
95
+ return parsed;
96
+ }
97
+
98
+ function normalizeAvailabilityEntry(value, label, { countRequired = true } = {}) {
99
+ const entry = object(value, label);
100
+ knownFields(entry, ['available', 'count'], label);
101
+ if (typeof entry.available !== 'boolean') fail(`${label}.available must be boolean`);
102
+ if (countRequired && entry.count === undefined) fail(`${label}.count is required`);
103
+ const result = { available: entry.available };
104
+ if (entry.count !== undefined || countRequired) result.count = boundedCount(entry.count, `${label}.count`);
105
+ if (result.count === 0 && result.available && label !== 'standard model availability') {
106
+ fail(`${label} cannot be available with zero entries`);
107
+ }
108
+ return result;
109
+ }
110
+
111
+ function normalizePersona(persona, index) {
112
+ object(persona, `active persona ${index + 1}`);
113
+ knownFields(persona, ['id', 'name', 'tier', 'category', 'description', 'matchedSignals', 'engagementReason'], `active persona ${index + 1}`);
114
+ const tier = token(persona.tier, `active persona ${index + 1} tier`);
115
+ if (!tiers.has(tier)) fail(`active persona ${index + 1} tier is unsupported`);
116
+ return {
117
+ id: token(persona.id, `active persona ${index + 1} ID`),
118
+ name: text(persona.name, `active persona ${index + 1} name`, 160),
119
+ tier,
120
+ category: text(persona.category ?? '', `active persona ${index + 1} category`, 120, { optional: true }),
121
+ description: text(persona.description ?? '', `active persona ${index + 1} description`, 500, { optional: true }),
122
+ matchedSignals: uniqueTexts(persona.matchedSignals ?? [], `active persona ${index + 1} matched signals`, 20, 120),
123
+ engagementReason: text(persona.engagementReason, `active persona ${index + 1} engagement reason`, 500),
124
+ };
125
+ }
126
+
127
+ export function normalizePrototypePersonaEngagement(value, expectedStage) {
128
+ const engagement = object(value, 'persona engagement');
129
+ knownFields(engagement, ['schema', 'stage', 'selectionFingerprint', 'activePersonas', 'availability', 'baseline', 'notices', 'authority'], 'persona engagement');
130
+ if (engagement.schema !== 'ewai.prototype-persona-engagement/v1') fail('persona engagement schema is unsupported');
131
+ const stage = token(engagement.stage, 'persona engagement stage');
132
+ if (!['plan', 'design'].includes(stage)) fail('persona engagement stage must be plan or design');
133
+ if (expectedStage && stage !== expectedStage) fail(`persona engagement stage must be ${expectedStage}`);
134
+ const activePersonas = list(engagement.activePersonas, 'active personas', 8)
135
+ .map(normalizePersona)
136
+ .sort((left, right) => left.id.localeCompare(right.id));
137
+ if (!activePersonas.length) fail('at least one active persona is required');
138
+ if (new Set(activePersonas.map(({ id }) => id)).size !== activePersonas.length) fail('active personas contains duplicate IDs');
139
+
140
+ const availability = object(engagement.availability, 'persona availability');
141
+ knownFields(availability, ['standardModel', 'core', 'project', 'personal', 'premium'], 'persona availability');
142
+ const normalizedAvailability = {
143
+ standardModel: normalizeAvailabilityEntry(availability.standardModel, 'standard model availability', { countRequired: false }),
144
+ core: normalizeAvailabilityEntry(availability.core, 'core persona availability'),
145
+ project: normalizeAvailabilityEntry(availability.project, 'project persona availability'),
146
+ personal: normalizeAvailabilityEntry(availability.personal, 'personal persona availability'),
147
+ premium: normalizeAvailabilityEntry(availability.premium, 'premium persona availability'),
148
+ };
149
+ if (!normalizedAvailability.standardModel.available) fail('the standard model baseline must be available');
150
+ for (const tier of ['core', 'project', 'personal', 'premium']) {
151
+ const selected = activePersonas.filter((persona) => persona.tier === tier).length;
152
+ if (selected > normalizedAvailability[tier].count) fail(`active ${tier} personas exceed the available count`);
153
+ }
154
+
155
+ const baseline = object(engagement.baseline, 'persona baseline');
156
+ knownFields(baseline, ['completeWithoutOptionalPersonas'], 'persona baseline');
157
+ if (baseline.completeWithoutOptionalPersonas !== true) fail('prototype review must remain complete without optional personal or premium personas');
158
+ const authority = object(engagement.authority, 'persona authority');
159
+ knownFields(authority, ['advisory', 'maySelectPrototype', 'mayApproveManualQa'], 'persona authority');
160
+ if (authority.advisory !== true || authority.maySelectPrototype !== false || authority.mayApproveManualQa !== false) {
161
+ fail('persona authority must remain advisory and may not select prototypes or approve Manual QA');
162
+ }
163
+ const normalized = {
164
+ schema: engagement.schema,
165
+ stage,
166
+ activePersonas,
167
+ availability: normalizedAvailability,
168
+ baseline: { completeWithoutOptionalPersonas: true },
169
+ notices: uniqueTexts(engagement.notices ?? [], 'persona engagement notices', 10, 500),
170
+ authority: { advisory: true, maySelectPrototype: false, mayApproveManualQa: false },
171
+ };
172
+ const calculatedFingerprint = hash({
173
+ stage: normalized.stage,
174
+ activePersonas: normalized.activePersonas.map(({ id, tier, matchedSignals }) => ({ id, tier, matchedSignals })),
175
+ availability: normalized.availability,
176
+ });
177
+ normalized.selectionFingerprint = engagement.selectionFingerprint
178
+ ? digest(engagement.selectionFingerprint, 'persona selection fingerprint')
179
+ : calculatedFingerprint;
180
+ return normalized;
181
+ }
182
+
183
+ function normalizeDesignSystem(value) {
184
+ const designSystem = object(value, 'design system');
185
+ knownFields(designSystem, ['id', 'effectiveDigest'], 'design system');
186
+ return {
187
+ id: token(designSystem.id, 'design system ID'),
188
+ effectiveDigest: digest(designSystem.effectiveDigest, 'design system effective digest'),
189
+ };
190
+ }
191
+
192
+ function normalizeScreen(value, index) {
193
+ const screen = object(value, `plan screen ${index + 1}`);
194
+ knownFields(screen, ['id', 'title', 'purpose', 'userOutcomes', 'evidenceRefs'], `plan screen ${index + 1}`);
195
+ return {
196
+ id: token(screen.id, `plan screen ${index + 1} ID`),
197
+ title: text(screen.title, `plan screen ${index + 1} title`, 160),
198
+ purpose: text(screen.purpose, `plan screen ${index + 1} purpose`, 1000),
199
+ userOutcomes: uniqueTexts(screen.userOutcomes, `plan screen ${index + 1} user outcomes`, 30, 500),
200
+ evidenceRefs: uniqueTokens(screen.evidenceRefs, `plan screen ${index + 1} evidence references`, 50),
201
+ };
202
+ }
203
+
204
+ function normalizeJourney(value, index, screenIds) {
205
+ const journey = object(value, `plan journey ${index + 1}`);
206
+ knownFields(journey, ['id', 'actor', 'outcome', 'screenRefs'], `plan journey ${index + 1}`);
207
+ const screenRefs = uniqueTokens(journey.screenRefs, `plan journey ${index + 1} screen references`, 30);
208
+ for (const reference of screenRefs) if (!screenIds.has(reference)) fail(`plan journey ${index + 1} references unknown screen ${reference}`);
209
+ return {
210
+ id: token(journey.id, `plan journey ${index + 1} ID`),
211
+ actor: token(journey.actor, `plan journey ${index + 1} actor`),
212
+ outcome: text(journey.outcome, `plan journey ${index + 1} outcome`, 1000),
213
+ screenRefs,
214
+ };
215
+ }
216
+
217
+ function normalizePlan(value) {
218
+ const plan = object(value, 'prototype plan');
219
+ knownFields(plan, ['summary', 'screens', 'journeys'], 'prototype plan');
220
+ const screens = list(plan.screens, 'prototype plan screens', 100).map(normalizeScreen).sort((left, right) => left.id.localeCompare(right.id));
221
+ if (!screens.length) fail('prototype plan requires at least one screen');
222
+ if (new Set(screens.map(({ id }) => id)).size !== screens.length) fail('prototype plan contains duplicate screen IDs');
223
+ const screenIds = new Set(screens.map(({ id }) => id));
224
+ const journeys = list(plan.journeys, 'prototype plan journeys', 100).map((journey, index) => normalizeJourney(journey, index, screenIds)).sort((left, right) => left.id.localeCompare(right.id));
225
+ if (!journeys.length) fail('prototype plan requires at least one journey');
226
+ if (new Set(journeys.map(({ id }) => id)).size !== journeys.length) fail('prototype plan contains duplicate journey IDs');
227
+ return {
228
+ summary: text(plan.summary, 'prototype plan summary', 2000),
229
+ screens,
230
+ journeys,
231
+ };
232
+ }
233
+
234
+ function normalizePlanInput(value) {
235
+ const input = object(value, 'prototype plan input');
236
+ knownFields(input, ['schema', 'deliverySlug', 'intentDigest', 'designSystem', 'plan', 'engagement', 'predecessorDigest'], 'prototype plan input');
237
+ if (input.schema !== 'ewai.prototype-plan-input/v1') fail('prototype plan input schema is unsupported');
238
+ return {
239
+ schema: input.schema,
240
+ deliverySlug: token(input.deliverySlug, 'delivery slug'),
241
+ intentDigest: digest(input.intentDigest, 'intent digest'),
242
+ designSystem: normalizeDesignSystem(input.designSystem),
243
+ plan: normalizePlan(input.plan),
244
+ engagement: normalizePrototypePersonaEngagement(input.engagement, 'plan'),
245
+ predecessorDigest: digest(input.predecessorDigest ?? '', 'predecessor digest', { optional: true }),
246
+ };
247
+ }
248
+
249
+ export function preparePrototypePlanReview(input, options = {}) {
250
+ const normalized = normalizePlanInput(input);
251
+ const identity = {
252
+ schema: 'ewai.prototype-plan-identity/v1',
253
+ deliverySlug: normalized.deliverySlug,
254
+ intentDigest: normalized.intentDigest,
255
+ designSystem: normalized.designSystem,
256
+ plan: normalized.plan,
257
+ engagement: normalized.engagement,
258
+ predecessorDigest: normalized.predecessorDigest,
259
+ };
260
+ return {
261
+ schema: 'ewai.prototype-plan-preparation/v1',
262
+ preparedAt: options.now ?? new Date().toISOString(),
263
+ preparationDigest: hash(identity),
264
+ inputFingerprint: hash({
265
+ deliverySlug: normalized.deliverySlug,
266
+ intentDigest: normalized.intentDigest,
267
+ designSystem: normalized.designSystem,
268
+ predecessorDigest: normalized.predecessorDigest,
269
+ }),
270
+ planFingerprint: hash(normalized.plan),
271
+ personaFingerprint: hash(normalized.engagement),
272
+ deliverySlug: normalized.deliverySlug,
273
+ intentDigest: normalized.intentDigest,
274
+ designSystem: normalized.designSystem,
275
+ plan: normalized.plan,
276
+ engagement: normalized.engagement,
277
+ predecessorDigest: normalized.predecessorDigest,
278
+ authority: {
279
+ personasAreAdvisory: true,
280
+ assessmentsMustBeExplicit: true,
281
+ humanPrototypeSelectionRequired: true,
282
+ manualQaRemainsSeparate: true,
283
+ },
284
+ };
285
+ }
286
+
287
+ function validatePlanPreparation(value) {
288
+ const preparation = object(value, 'prototype plan preparation');
289
+ knownFields(preparation, [
290
+ 'schema', 'preparedAt', 'preparationDigest', 'inputFingerprint', 'planFingerprint', 'personaFingerprint',
291
+ 'deliverySlug', 'intentDigest', 'designSystem', 'plan', 'engagement', 'predecessorDigest', 'authority',
292
+ ], 'prototype plan preparation');
293
+ if (preparation.schema !== 'ewai.prototype-plan-preparation/v1') fail('prototype plan preparation schema is unsupported');
294
+ const rebuilt = preparePrototypePlanReview({
295
+ schema: 'ewai.prototype-plan-input/v1',
296
+ deliverySlug: preparation.deliverySlug,
297
+ intentDigest: preparation.intentDigest,
298
+ designSystem: preparation.designSystem,
299
+ plan: preparation.plan,
300
+ engagement: preparation.engagement,
301
+ predecessorDigest: preparation.predecessorDigest,
302
+ }, { now: preparation.preparedAt });
303
+ for (const field of ['preparationDigest', 'inputFingerprint', 'planFingerprint', 'personaFingerprint']) {
304
+ if (preparation[field] !== rebuilt[field]) fail(`${field} does not match the preparation content`);
305
+ }
306
+ const authority = object(preparation.authority, 'preparation authority');
307
+ if (authority.personasAreAdvisory !== true || authority.assessmentsMustBeExplicit !== true
308
+ || authority.humanPrototypeSelectionRequired !== true || authority.manualQaRemainsSeparate !== true) {
309
+ fail('preparation authority is invalid');
310
+ }
311
+ return rebuilt;
312
+ }
313
+
314
+ function normalizeFinding(value, index, personaIds) {
315
+ const finding = object(value, `finding ${index + 1}`);
316
+ knownFields(finding, ['id', 'personaId', 'concernCode', 'severity', 'observation', 'recommendation', 'evidenceRefs'], `finding ${index + 1}`);
317
+ const identity = {
318
+ personaId: token(finding.personaId, `finding ${index + 1} persona ID`),
319
+ concernCode: token(finding.concernCode, `finding ${index + 1} concern code`),
320
+ evidenceRefs: uniqueTokens(finding.evidenceRefs, `finding ${index + 1} evidence references`, 50),
321
+ };
322
+ if (!personaIds.has(identity.personaId)) fail(`finding ${index + 1} references inactive persona ${identity.personaId}`);
323
+ if (!identity.evidenceRefs.length) fail(`finding ${index + 1} requires evidence references`);
324
+ const severity = token(finding.severity, `finding ${index + 1} severity`);
325
+ if (!severities.has(severity)) fail(`finding ${index + 1} severity is unsupported`);
326
+ const id = prototypeFindingId(identity);
327
+ if (finding.id !== undefined && finding.id !== id) fail(`finding ${index + 1} ID does not match its stable identity`);
328
+ return {
329
+ id,
330
+ ...identity,
331
+ severity,
332
+ observation: text(finding.observation, `finding ${index + 1} observation`, 2000),
333
+ recommendation: text(finding.recommendation, `finding ${index + 1} recommendation`, 2000),
334
+ };
335
+ }
336
+
337
+ export function prototypeFindingId(value) {
338
+ const identity = {
339
+ personaId: token(value?.personaId, 'finding identity persona ID'),
340
+ concernCode: token(value?.concernCode, 'finding identity concern code'),
341
+ evidenceRefs: uniqueTokens(value?.evidenceRefs, 'finding identity evidence references', 50),
342
+ };
343
+ return `FIND-${sha256(JSON.stringify(canonical(identity))).slice(0, 16)}`;
344
+ }
345
+
346
+ function normalizeAssessment(value, index, findingIds) {
347
+ const assessment = object(value, `assessment ${index + 1}`);
348
+ knownFields(assessment, ['findingId', 'disposition', 'rationale', 'modification', 'owner'], `assessment ${index + 1}`);
349
+ const findingId = token(assessment.findingId, `assessment ${index + 1} finding ID`);
350
+ if (!findingIds.has(findingId)) fail(`assessment ${index + 1} references unknown finding ${findingId}`);
351
+ const disposition = token(assessment.disposition, `assessment ${index + 1} disposition`);
352
+ if (!dispositions.has(disposition)) fail(`assessment ${index + 1} disposition is unsupported`);
353
+ const modification = text(assessment.modification ?? '', `assessment ${index + 1} modification`, 2000, { optional: true });
354
+ if (disposition === 'incorporate-with-modification' && modification.length < 12) {
355
+ fail(`assessment ${index + 1} requires a substantive modification`);
356
+ }
357
+ return {
358
+ findingId,
359
+ disposition,
360
+ rationale: text(assessment.rationale, `assessment ${index + 1} rationale`, 2000),
361
+ modification,
362
+ owner: text(assessment.owner ?? '', `assessment ${index + 1} owner`, 160, { optional: true }),
363
+ };
364
+ }
365
+
366
+ function deriveNextAction(findings, assessments) {
367
+ const byId = new Map(findings.map((finding) => [finding.id, finding]));
368
+ if (assessments.some(({ disposition }) => disposition === 'escalate')) return 'human-decision-required';
369
+ if (assessments.some((assessment) => assessment.disposition === 'defer' && ['material', 'critical'].includes(byId.get(assessment.findingId)?.severity))) {
370
+ return 'human-decision-required';
371
+ }
372
+ if (assessments.some(({ disposition }) => ['incorporate', 'incorporate-with-modification'].includes(disposition))) return 'iterate';
373
+ return 'ready-for-human-selection';
374
+ }
375
+
376
+ export function recordPrototypePlanReview(projectRoot, preparationInput, submissionInput, options = {}) {
377
+ const preparation = validatePlanPreparation(preparationInput);
378
+ const submission = object(submissionInput, 'prototype plan review submission');
379
+ knownFields(submission, ['schema', 'expectedPreparationDigest', 'reviewedBy', 'findings', 'assessments'], 'prototype plan review submission');
380
+ if (submission.schema !== 'ewai.prototype-plan-review-submission/v1') fail('prototype plan review submission schema is unsupported');
381
+ if (digest(submission.expectedPreparationDigest, 'expected preparation digest') !== preparation.preparationDigest) {
382
+ fail('expected preparation digest does not match; prepare a newer review before recording');
383
+ }
384
+ const personaIds = new Set(preparation.engagement.activePersonas.map(({ id }) => id));
385
+ const findings = list(submission.findings, 'findings', 200).map((finding, index) => normalizeFinding(finding, index, personaIds)).sort((left, right) => left.id.localeCompare(right.id));
386
+ if (new Set(findings.map(({ id }) => id)).size !== findings.length) fail('findings contains duplicate stable identities');
387
+ const findingIds = new Set(findings.map(({ id }) => id));
388
+ const assessments = list(submission.assessments, 'assessments', 200).map((assessment, index) => normalizeAssessment(assessment, index, findingIds)).sort((left, right) => left.findingId.localeCompare(right.findingId));
389
+ if (new Set(assessments.map(({ findingId }) => findingId)).size !== assessments.length) fail('duplicate assessment for a finding');
390
+ const unassessed = findings.filter(({ id }) => !assessments.some(({ findingId }) => findingId === id)).map(({ id }) => id);
391
+ if (unassessed.length && !options.allowIncomplete) fail(`an assessment is required for every finding; unassessed: ${unassessed.join(', ')}`);
392
+ if (assessments.length > findings.length) fail('assessments cannot exceed findings');
393
+
394
+ const identity = {
395
+ schema: 'ewai.prototype-plan-review-identity/v1',
396
+ preparationDigest: preparation.preparationDigest,
397
+ inputFingerprint: preparation.inputFingerprint,
398
+ planFingerprint: preparation.planFingerprint,
399
+ personaFingerprint: preparation.personaFingerprint,
400
+ reviewedBy: text(submission.reviewedBy, 'reviewed by', 160),
401
+ findings,
402
+ assessments,
403
+ };
404
+ const contentDigest = hash(identity);
405
+ const review = {
406
+ schema: 'ewai.prototype-plan-review/v1',
407
+ reviewedAt: options.now ?? new Date().toISOString(),
408
+ contentDigest,
409
+ predecessorDigest: preparation.predecessorDigest,
410
+ preparationDigest: preparation.preparationDigest,
411
+ inputFingerprint: preparation.inputFingerprint,
412
+ planFingerprint: preparation.planFingerprint,
413
+ personaFingerprint: preparation.personaFingerprint,
414
+ deliverySlug: preparation.deliverySlug,
415
+ reviewedBy: identity.reviewedBy,
416
+ engagement: preparation.engagement,
417
+ findings,
418
+ assessments,
419
+ assessment: {
420
+ complete: unassessed.length === 0,
421
+ findingCount: findings.length,
422
+ assessedCount: assessments.length,
423
+ unassessedFindingIds: unassessed,
424
+ },
425
+ nextAction: unassessed.length ? 'assessment-required' : deriveNextAction(findings, assessments),
426
+ authority: preparation.authority,
427
+ };
428
+
429
+ if (options.persist === false || projectRoot === null) return { review, path: '' };
430
+ const root = prototypeReviewEvidenceRoot(projectRoot, preparation.deliverySlug);
431
+ const folder = resolve(root, 'plans');
432
+ mkdirSync(folder, { recursive: true });
433
+ const path = resolve(folder, `plan-${contentDigest.slice(7)}.json`);
434
+ const relativePath = relative(projectPaths(projectRoot).projectRoot, path).replaceAll('\\', '/');
435
+ if (existsSync(path)) {
436
+ const stored = JSON.parse(readFileSync(path, 'utf8'));
437
+ if (stored.contentDigest !== contentDigest) fail('immutable plan review evidence has conflicting content');
438
+ return { review: stored, path: relativePath };
439
+ }
440
+ writeFileSync(path, `${JSON.stringify(review, null, 2)}\n`, { encoding: 'utf8', flag: 'wx', mode: 0o600 });
441
+ return { review, path: relativePath };
442
+ }
443
+
444
+ function boundedCycle(value, label) {
445
+ const parsed = Number(value);
446
+ if (!Number.isInteger(parsed) || parsed < 1 || parsed > 3) fail(`${label} must be an integer from 1 to 3`);
447
+ return parsed;
448
+ }
449
+
450
+ function normalizePrototype(value) {
451
+ const prototype = object(value, 'prototype');
452
+ knownFields(prototype, ['manifestDigest', 'entryPath'], 'prototype');
453
+ const entryPath = token(prototype.entryPath, 'prototype entry path');
454
+ if (!entryPath.startsWith('ui-design-assets/prototypes/') || !/\.html?$/i.test(entryPath)) {
455
+ fail('prototype entry path must identify an HTML file inside ui-design-assets/prototypes');
456
+ }
457
+ return {
458
+ manifestDigest: digest(prototype.manifestDigest, 'prototype manifest digest'),
459
+ entryPath,
460
+ };
461
+ }
462
+
463
+ function normalizeEvidenceChannels(value) {
464
+ const statuses = new Set(['available', 'not-collected', 'not-applicable', 'pending-human']);
465
+ const byChannel = new Map();
466
+ for (const [index, entry] of list(value, 'evidence channels', PROTOTYPE_EVIDENCE_CHANNELS.length).entries()) {
467
+ const evidence = object(entry, `evidence channel ${index + 1}`);
468
+ knownFields(evidence, ['channel', 'status', 'evidenceRefs', 'note'], `evidence channel ${index + 1}`);
469
+ const channel = token(evidence.channel, `evidence channel ${index + 1} name`);
470
+ if (!PROTOTYPE_EVIDENCE_CHANNELS.includes(channel)) fail(`evidence channel ${channel} is unsupported`);
471
+ if (byChannel.has(channel)) fail(`evidence channel ${channel} is duplicated`);
472
+ const status = token(evidence.status, `${channel} evidence status`);
473
+ if (!statuses.has(status)) fail(`${channel} evidence status is unsupported`);
474
+ const evidenceRefs = uniqueTokens(evidence.evidenceRefs ?? [], `${channel} evidence references`, 100);
475
+ if (status === 'available' && !evidenceRefs.length) fail(`${channel} available evidence requires at least one reference`);
476
+ if (status !== 'available' && evidenceRefs.length) fail(`${channel} unavailable evidence may not claim evidence references`);
477
+ byChannel.set(channel, {
478
+ channel,
479
+ status,
480
+ evidenceRefs,
481
+ note: text(evidence.note, `${channel} evidence note`, 1000),
482
+ });
483
+ }
484
+ if (byChannel.size !== PROTOTYPE_EVIDENCE_CHANNELS.length) fail('every evidence channel must be represented exactly once');
485
+ for (const required of ['source', 'rendered-viewport']) {
486
+ if (byChannel.get(required)?.status !== 'available') fail(`${required} evidence must be available before persona design review`);
487
+ }
488
+ if (byChannel.get('manual-qa')?.status === 'available') fail('Manual QA evidence cannot be granted by persona design review');
489
+ if (byChannel.get('release')?.status === 'available') fail('release evidence cannot be granted by persona design review');
490
+ return PROTOTYPE_EVIDENCE_CHANNELS.map((channel) => byChannel.get(channel));
491
+ }
492
+
493
+ function normalizeCycleInput(value) {
494
+ const input = object(value, 'prototype cycle input');
495
+ knownFields(input, [
496
+ 'schema', 'deliverySlug', 'planReviewDigest', 'cycleNumber', 'maxCycles', 'predecessorDigest',
497
+ 'prototype', 'evidenceChannels', 'engagement',
498
+ ], 'prototype cycle input');
499
+ if (input.schema !== 'ewai.prototype-cycle-input/v1') fail('prototype cycle input schema is unsupported');
500
+ const cycleNumber = boundedCycle(input.cycleNumber, 'cycle number');
501
+ const maxCycles = boundedCycle(input.maxCycles, 'maximum cycles');
502
+ if (cycleNumber > maxCycles) fail('cycle number cannot exceed maximum cycles');
503
+ const predecessorDigest = digest(input.predecessorDigest ?? '', 'predecessor digest', { optional: true });
504
+ if (cycleNumber === 1 && predecessorDigest) fail('the first cycle may not declare a predecessor digest');
505
+ if (cycleNumber > 1 && !predecessorDigest) fail('cycles after the first require a predecessor digest');
506
+ return {
507
+ schema: input.schema,
508
+ deliverySlug: token(input.deliverySlug, 'delivery slug'),
509
+ planReviewDigest: digest(input.planReviewDigest, 'plan review digest'),
510
+ cycleNumber,
511
+ maxCycles,
512
+ predecessorDigest,
513
+ prototype: normalizePrototype(input.prototype),
514
+ evidenceChannels: normalizeEvidenceChannels(input.evidenceChannels),
515
+ engagement: normalizePrototypePersonaEngagement(input.engagement, 'design'),
516
+ };
517
+ }
518
+
519
+ export function preparePrototypeCycleReview(input, options = {}) {
520
+ const normalized = normalizeCycleInput(input);
521
+ const identity = {
522
+ schema: 'ewai.prototype-cycle-identity/v1',
523
+ deliverySlug: normalized.deliverySlug,
524
+ planReviewDigest: normalized.planReviewDigest,
525
+ cycleNumber: normalized.cycleNumber,
526
+ maxCycles: normalized.maxCycles,
527
+ predecessorDigest: normalized.predecessorDigest,
528
+ prototype: normalized.prototype,
529
+ evidenceChannels: normalized.evidenceChannels,
530
+ engagement: normalized.engagement,
531
+ };
532
+ return {
533
+ ...normalized,
534
+ schema: 'ewai.prototype-cycle-preparation/v1',
535
+ preparedAt: options.now ?? new Date().toISOString(),
536
+ preparationDigest: hash(identity),
537
+ inputFingerprint: hash({
538
+ deliverySlug: normalized.deliverySlug,
539
+ planReviewDigest: normalized.planReviewDigest,
540
+ cycleNumber: normalized.cycleNumber,
541
+ maxCycles: normalized.maxCycles,
542
+ predecessorDigest: normalized.predecessorDigest,
543
+ prototype: normalized.prototype,
544
+ }),
545
+ evidenceFingerprint: hash(normalized.evidenceChannels),
546
+ personaFingerprint: hash(normalized.engagement),
547
+ authority: {
548
+ personasAreAdvisory: true,
549
+ assessmentsMustBeExplicit: true,
550
+ humanPrototypeSelectionRequired: true,
551
+ manualQaRemainsSeparate: true,
552
+ releaseEvidenceRemainsSeparate: true,
553
+ },
554
+ };
555
+ }
556
+
557
+ function validateCyclePreparation(value) {
558
+ const preparation = object(value, 'prototype cycle preparation');
559
+ knownFields(preparation, [
560
+ 'schema', 'preparedAt', 'preparationDigest', 'inputFingerprint', 'evidenceFingerprint', 'personaFingerprint',
561
+ 'deliverySlug', 'planReviewDigest', 'cycleNumber', 'maxCycles', 'predecessorDigest', 'prototype',
562
+ 'evidenceChannels', 'engagement', 'authority',
563
+ ], 'prototype cycle preparation');
564
+ if (preparation.schema !== 'ewai.prototype-cycle-preparation/v1') fail('prototype cycle preparation schema is unsupported');
565
+ const rebuilt = preparePrototypeCycleReview({
566
+ schema: 'ewai.prototype-cycle-input/v1',
567
+ deliverySlug: preparation.deliverySlug,
568
+ planReviewDigest: preparation.planReviewDigest,
569
+ cycleNumber: preparation.cycleNumber,
570
+ maxCycles: preparation.maxCycles,
571
+ predecessorDigest: preparation.predecessorDigest,
572
+ prototype: preparation.prototype,
573
+ evidenceChannels: preparation.evidenceChannels,
574
+ engagement: preparation.engagement,
575
+ }, { now: preparation.preparedAt });
576
+ for (const field of ['preparationDigest', 'inputFingerprint', 'evidenceFingerprint', 'personaFingerprint']) {
577
+ if (preparation[field] !== rebuilt[field]) fail(`${field} does not match the cycle preparation content`);
578
+ }
579
+ return rebuilt;
580
+ }
581
+
582
+ function deriveCycleNextAction(findings, assessments, cycleNumber, maxCycles) {
583
+ const base = deriveNextAction(findings, assessments);
584
+ if (base === 'iterate' && cycleNumber >= maxCycles) return 'human-decision-required';
585
+ return base;
586
+ }
587
+
588
+ export function recordPrototypeCycleReview(projectRoot, preparationInput, submissionInput, options = {}) {
589
+ const preparation = validateCyclePreparation(preparationInput);
590
+ const submission = object(submissionInput, 'prototype cycle review submission');
591
+ knownFields(submission, ['schema', 'expectedPreparationDigest', 'reviewedBy', 'findings', 'assessments'], 'prototype cycle review submission');
592
+ if (submission.schema !== 'ewai.prototype-cycle-review-submission/v1') fail('prototype cycle review submission schema is unsupported');
593
+ if (digest(submission.expectedPreparationDigest, 'expected preparation digest') !== preparation.preparationDigest) {
594
+ fail('expected preparation digest does not match; prepare a newer design cycle before recording');
595
+ }
596
+ const personaIds = new Set(preparation.engagement.activePersonas.map(({ id }) => id));
597
+ const findings = list(submission.findings, 'findings', 200).map((finding, index) => normalizeFinding(finding, index, personaIds)).sort((left, right) => left.id.localeCompare(right.id));
598
+ if (new Set(findings.map(({ id }) => id)).size !== findings.length) fail('findings contains duplicate stable identities');
599
+ const findingIds = new Set(findings.map(({ id }) => id));
600
+ const assessments = list(submission.assessments, 'assessments', 200).map((assessment, index) => normalizeAssessment(assessment, index, findingIds)).sort((left, right) => left.findingId.localeCompare(right.findingId));
601
+ if (new Set(assessments.map(({ findingId }) => findingId)).size !== assessments.length) fail('duplicate assessment for a finding');
602
+ const unassessed = findings.filter(({ id }) => !assessments.some(({ findingId }) => findingId === id)).map(({ id }) => id);
603
+ if (unassessed.length && !options.allowIncomplete) fail(`an assessment is required for every finding; unassessed: ${unassessed.join(', ')}`);
604
+
605
+ const identity = {
606
+ schema: 'ewai.prototype-cycle-review-identity/v1',
607
+ preparationDigest: preparation.preparationDigest,
608
+ inputFingerprint: preparation.inputFingerprint,
609
+ evidenceFingerprint: preparation.evidenceFingerprint,
610
+ personaFingerprint: preparation.personaFingerprint,
611
+ reviewedBy: text(submission.reviewedBy, 'reviewed by', 160),
612
+ findings,
613
+ assessments,
614
+ };
615
+ const contentDigest = hash(identity);
616
+ const review = {
617
+ schema: 'ewai.prototype-cycle-review/v1',
618
+ reviewedAt: options.now ?? new Date().toISOString(),
619
+ contentDigest,
620
+ predecessorDigest: preparation.predecessorDigest,
621
+ planReviewDigest: preparation.planReviewDigest,
622
+ preparationDigest: preparation.preparationDigest,
623
+ inputFingerprint: preparation.inputFingerprint,
624
+ evidenceFingerprint: preparation.evidenceFingerprint,
625
+ personaFingerprint: preparation.personaFingerprint,
626
+ deliverySlug: preparation.deliverySlug,
627
+ cycleNumber: preparation.cycleNumber,
628
+ maxCycles: preparation.maxCycles,
629
+ prototype: preparation.prototype,
630
+ evidenceChannels: preparation.evidenceChannels,
631
+ engagement: preparation.engagement,
632
+ reviewedBy: identity.reviewedBy,
633
+ findings,
634
+ assessments,
635
+ assessment: {
636
+ complete: unassessed.length === 0,
637
+ findingCount: findings.length,
638
+ assessedCount: assessments.length,
639
+ unassessedFindingIds: unassessed,
640
+ },
641
+ nextAction: unassessed.length
642
+ ? 'assessment-required'
643
+ : deriveCycleNextAction(findings, assessments, preparation.cycleNumber, preparation.maxCycles),
644
+ authority: preparation.authority,
645
+ };
646
+
647
+ if (options.persist === false || projectRoot === null) return { review, path: '' };
648
+ const folder = resolve(prototypeReviewEvidenceRoot(projectRoot, preparation.deliverySlug), 'cycles');
649
+ mkdirSync(folder, { recursive: true });
650
+ const path = resolve(folder, `cycle-${preparation.cycleNumber}-${contentDigest.slice(7)}.json`);
651
+ const relativePath = relative(projectPaths(projectRoot).projectRoot, path).replaceAll('\\', '/');
652
+ if (existsSync(path)) {
653
+ const stored = JSON.parse(readFileSync(path, 'utf8'));
654
+ if (stored.contentDigest !== contentDigest) fail('immutable cycle review evidence has conflicting content');
655
+ return { review: stored, path: relativePath };
656
+ }
657
+ writeFileSync(path, `${JSON.stringify(review, null, 2)}\n`, { encoding: 'utf8', flag: 'wx', mode: 0o600 });
658
+ return { review, path: relativePath };
659
+ }
660
+
661
+ function safeDeliveryRoot(projectRoot, slug) {
662
+ const project = projectPaths(projectRoot);
663
+ const safeSlug = token(slug, 'delivery slug');
664
+ const root = resolve(project.buildRoot, safeSlug);
665
+ const rel = relative(project.buildRoot, root);
666
+ if (!rel || rel.startsWith('..') || isAbsolute(rel)) fail('delivery slug must resolve inside the Build root');
667
+ return root;
668
+ }
669
+
670
+ function prototypeReviewEvidenceRoot(projectRoot, slug) {
671
+ return resolve(safeDeliveryRoot(projectRoot, slug), 'ui-design-assets/prototype-iterations');
672
+ }
673
+
674
+ function readJsonFiles(folder) {
675
+ if (!existsSync(folder)) return [];
676
+ if (lstatSync(folder).isSymbolicLink()) fail(`evidence folder may not be a symbolic link: ${basename(folder)}`);
677
+ return readdirSync(folder, { withFileTypes: true })
678
+ .filter((entry) => entry.isFile() && entry.name.endsWith('.json'))
679
+ .map((entry) => {
680
+ const path = resolve(folder, entry.name);
681
+ if (lstatSync(path).isSymbolicLink()) fail(`evidence file may not be a symbolic link: ${entry.name}`);
682
+ return JSON.parse(readFileSync(path, 'utf8'));
683
+ });
684
+ }
685
+
686
+ export function readPrototypeReviewStatus(projectRoot, slug) {
687
+ const root = prototypeReviewEvidenceRoot(projectRoot, slug);
688
+ const planReviews = readJsonFiles(resolve(root, 'plans')).sort((left, right) => left.reviewedAt.localeCompare(right.reviewedAt));
689
+ const cycleReviews = readJsonFiles(resolve(root, 'cycles')).sort((left, right) => left.reviewedAt.localeCompare(right.reviewedAt));
690
+ return {
691
+ schema: 'ewai.prototype-review-status/v1',
692
+ deliverySlug: token(slug, 'delivery slug'),
693
+ planReviews,
694
+ cycleReviews,
695
+ currentPlanReview: planReviews.at(-1) ?? null,
696
+ currentCycleReview: cycleReviews.at(-1) ?? null,
697
+ authority: {
698
+ evidenceIsAdvisory: true,
699
+ humanPrototypeSelectionRequired: true,
700
+ manualQaRemainsSeparate: true,
701
+ },
702
+ };
703
+ }
704
+
705
+ function change(left, right, explanation) {
706
+ return { status: left === right ? 'same' : 'changed', left, right, explanation };
707
+ }
708
+
709
+ export function comparePrototypeReviews(leftInput, rightInput) {
710
+ const left = object(leftInput, 'left prototype review');
711
+ const right = object(rightInput, 'right prototype review');
712
+ const changes = {
713
+ inputs: change(left.inputFingerprint, right.inputFingerprint, 'The intent, design system, predecessor, or reviewed prototype input changed.'),
714
+ personas: change(left.personaFingerprint, right.personaFingerprint, 'The stage-specific active or available persona set changed.'),
715
+ findings: change(hash(left.findings ?? []), hash(right.findings ?? []), 'Persona findings changed after input and persona selection.'),
716
+ assessments: change(hash(left.assessments ?? []), hash(right.assessments ?? []), 'Finding dispositions or accountable rationale changed.'),
717
+ output: change(left.contentDigest, right.contentDigest, 'The immutable review output changed.'),
718
+ };
719
+ const upstreamChanged = ['inputs', 'personas', 'findings', 'assessments'].some((field) => changes[field].status === 'changed');
720
+ const unexplained = changes.output.status === 'changed' && !upstreamChanged
721
+ ? [{ field: 'output', reason: 'The output changed without an observable input, persona, finding, or assessment change.' }]
722
+ : [];
723
+ return {
724
+ schema: 'ewai.prototype-review-comparison/v1',
725
+ order: ['inputs', 'personas', 'findings', 'assessments', 'output'],
726
+ changes,
727
+ reproducible: unexplained.length === 0,
728
+ unexplained,
729
+ };
730
+ }