@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,448 @@
1
+ import { createHash, randomUUID } from 'node:crypto';
2
+ import {
3
+ existsSync,
4
+ mkdirSync,
5
+ readFileSync,
6
+ renameSync,
7
+ unlinkSync,
8
+ writeFileSync,
9
+ } from 'node:fs';
10
+ import { dirname, resolve } from 'node:path';
11
+ import YAML from 'yaml';
12
+ import { z } from 'zod';
13
+ import { loadProjectConfig } from './project.mjs';
14
+
15
+ export const ORGANISATION_POLICY_BASELINE_PATH = 'SPECS/4.Constraints/organisation-policy/baseline.json';
16
+
17
+ const slug = z.string().regex(/^[a-z0-9]+(?:-[a-z0-9]+)*$/, 'must be a lower-case slug');
18
+ const organisationPackId = z.string().regex(/^org\.[a-z0-9]+(?:-[a-z0-9]+)*\.[a-z0-9]+(?:[.-][a-z0-9]+)*$/);
19
+ const valueId = z.string().min(1).max(120).regex(/^[a-z0-9]+(?:[.-][a-z0-9]+)*$/, 'must be a bounded lower-case identifier');
20
+ const text = (maximum) => z.string().trim().min(1).max(maximum);
21
+ const uniqueIds = (values) => new Set(values).size === values.length;
22
+ const outcomes = ['allow', 'deny', 'review-required', 'allow-with-controls', 'unassessed'];
23
+
24
+ const sourceReferenceSchema = z.object({
25
+ id: slug,
26
+ label: text(240),
27
+ }).strict();
28
+
29
+ const reviewRoleSchema = z.object({
30
+ id: slug,
31
+ name: text(160),
32
+ }).strict();
33
+
34
+ const conditionValues = z.array(valueId).min(1).max(40).refine(uniqueIds, 'must not contain duplicates');
35
+ const conditionSchema = z.object({
36
+ resource: conditionValues.optional(),
37
+ operation: conditionValues.optional(),
38
+ data_entity: conditionValues.optional(),
39
+ data_classification: conditionValues.optional(),
40
+ actor: conditionValues.optional(),
41
+ environment: conditionValues.optional(),
42
+ destination: conditionValues.optional(),
43
+ model: conditionValues.optional(),
44
+ ai_use: conditionValues.optional(),
45
+ retention: conditionValues.optional(),
46
+ }).strict().refine((value) => Object.keys(value).length > 0, 'must define at least one condition');
47
+
48
+ const controlSchema = z.object({
49
+ id: slug,
50
+ title: text(160),
51
+ description: text(800),
52
+ evidence: z.array(z.enum(['claim', 'task', 'automated-test', 'named-human', 'specialist-review'])).min(1).max(5).refine(uniqueIds, 'must not contain duplicate evidence routes'),
53
+ }).strict();
54
+
55
+ const exceptionSchema = z.object({
56
+ permitted: z.boolean(),
57
+ review_role: slug.optional(),
58
+ }).strict();
59
+
60
+ const ruleSchema = z.object({
61
+ id: slug,
62
+ title: text(160),
63
+ description: text(1200),
64
+ when: conditionSchema,
65
+ outcome: z.enum(outcomes),
66
+ review_role: slug.optional(),
67
+ controls: z.array(controlSchema).max(40).default([]),
68
+ exceptions: exceptionSchema,
69
+ }).strict();
70
+
71
+ const policySchema = z.object({
72
+ schema: z.literal('ewai.organisation-policy/v1'),
73
+ id: slug,
74
+ title: text(160),
75
+ version: z.string().regex(/^\d+\.\d+\.\d+$/),
76
+ publisher: z.object({ id: slug, name: text(160) }).strict(),
77
+ provenance: z.object({
78
+ kind: z.enum(['owner-declared', 'regulatory', 'contractual', 'adapted']),
79
+ summary: text(800),
80
+ sources: z.array(sourceReferenceSchema).min(1).max(40),
81
+ }).strict(),
82
+ review_roles: z.array(reviewRoleSchema).max(40).default([]),
83
+ unmatched_outcome: z.enum(outcomes),
84
+ rules: z.array(ruleSchema).min(1).max(500),
85
+ }).strict().superRefine((policy, context) => {
86
+ const duplicate = (items) => items.find((item, index) => items.indexOf(item) !== index);
87
+ const sourceDuplicate = duplicate(policy.provenance.sources.map(({ id }) => id));
88
+ if (sourceDuplicate) context.addIssue({ code: 'custom', path: ['provenance', 'sources'], message: `duplicate source reference: ${sourceDuplicate}` });
89
+ const roleDuplicate = duplicate(policy.review_roles.map(({ id }) => id));
90
+ if (roleDuplicate) context.addIssue({ code: 'custom', path: ['review_roles'], message: `duplicate review role: ${roleDuplicate}` });
91
+ const ruleDuplicate = duplicate(policy.rules.map(({ id }) => id));
92
+ if (ruleDuplicate) context.addIssue({ code: 'custom', path: ['rules'], message: `duplicate rule id: ${ruleDuplicate}` });
93
+ const roles = new Set(policy.review_roles.map(({ id }) => id));
94
+ for (const [index, rule] of policy.rules.entries()) {
95
+ const controlDuplicate = duplicate(rule.controls.map(({ id }) => id));
96
+ if (controlDuplicate) context.addIssue({ code: 'custom', path: ['rules', index, 'controls'], message: `duplicate control id: ${controlDuplicate}` });
97
+ if (rule.outcome === 'review-required' && !rule.review_role) {
98
+ context.addIssue({ code: 'custom', path: ['rules', index, 'review_role'], message: 'review-required rules must identify a review role' });
99
+ }
100
+ if (rule.outcome === 'allow-with-controls' && rule.controls.length === 0) {
101
+ context.addIssue({ code: 'custom', path: ['rules', index, 'controls'], message: 'allow-with-controls rules must define at least one control' });
102
+ }
103
+ if (rule.review_role && !roles.has(rule.review_role)) {
104
+ context.addIssue({ code: 'custom', path: ['rules', index, 'review_role'], message: `unknown review role: ${rule.review_role}` });
105
+ }
106
+ if (rule.exceptions.review_role && !roles.has(rule.exceptions.review_role)) {
107
+ context.addIssue({ code: 'custom', path: ['rules', index, 'exceptions', 'review_role'], message: `unknown review role: ${rule.exceptions.review_role}` });
108
+ }
109
+ if (rule.exceptions.permitted && !rule.exceptions.review_role) {
110
+ context.addIssue({ code: 'custom', path: ['rules', index, 'exceptions', 'review_role'], message: 'permitted exceptions require a review role' });
111
+ }
112
+ if (!rule.exceptions.permitted && rule.exceptions.review_role) {
113
+ context.addIssue({ code: 'custom', path: ['rules', index, 'exceptions', 'review_role'], message: 'prohibited exceptions may not nominate a review role' });
114
+ }
115
+ }
116
+ });
117
+
118
+ const forbiddenKeys = /^(?:script|scripts|command|commands|prompt|prompts|template|templates|expression|expressions|executable|secret|secrets|credential|credentials|token|tokens|remote|fetch|url|uri|endpoint|handler|hook)$/i;
119
+
120
+ function assertInert(value, path = 'policy') {
121
+ if (Array.isArray(value)) {
122
+ value.forEach((item, index) => assertInert(item, `${path}.${index}`));
123
+ return;
124
+ }
125
+ if (!value || typeof value !== 'object') return;
126
+ for (const [key, item] of Object.entries(value)) {
127
+ if (forbiddenKeys.test(key)) throw new Error(`Organisation policy contains forbidden executable or remote field at ${path}.${key}`);
128
+ assertInert(item, `${path}.${key}`);
129
+ }
130
+ }
131
+
132
+ function parsePolicy(value) {
133
+ if (typeof value !== 'string') return value;
134
+ try {
135
+ return YAML.parse(value);
136
+ } catch (error) {
137
+ throw new Error(`Organisation policy is invalid YAML or JSON: ${error.message}`);
138
+ }
139
+ }
140
+
141
+ function canonical(value) {
142
+ if (Array.isArray(value)) return value.map(canonical);
143
+ if (value && typeof value === 'object') {
144
+ return Object.fromEntries(Object.keys(value).sort().map((key) => [key, canonical(value[key])]));
145
+ }
146
+ return value;
147
+ }
148
+
149
+ function digest(value) {
150
+ return `sha256:${createHash('sha256').update(JSON.stringify(canonical(value))).digest('hex')}`;
151
+ }
152
+
153
+ function validationError(error) {
154
+ return error.issues?.map((issue) => `${issue.path.join('.') || 'policy'} ${issue.message}`).join('; ') ?? error.message;
155
+ }
156
+
157
+ export function validateOrganisationPolicy(value, options = {}) {
158
+ const parsed = parsePolicy(value);
159
+ assertInert(parsed);
160
+ let policy;
161
+ try {
162
+ policy = policySchema.parse(parsed);
163
+ } catch (error) {
164
+ throw new Error(`Organisation policy is invalid: ${validationError(error)}`);
165
+ }
166
+ if (options.publisherId && policy.publisher.id !== options.publisherId) {
167
+ throw new Error(`Organisation policy publisher ${policy.publisher.id} does not match Blueprint publisher ${options.publisherId}`);
168
+ }
169
+ if (options.contributionId && policy.id !== options.contributionId) {
170
+ throw new Error(`Organisation policy id ${policy.id} does not match Blueprint contribution ${options.contributionId}`);
171
+ }
172
+ return { policy, digest: digest(policy) };
173
+ }
174
+
175
+ function packPin(pack) {
176
+ return {
177
+ id: pack.id,
178
+ version: pack.version,
179
+ digest: pack.digest,
180
+ sourceClass: pack.sourceClass,
181
+ };
182
+ }
183
+
184
+ function effectivePolicyDigest(blueprintDigest, packs, contributions) {
185
+ return digest({
186
+ blueprintDigest,
187
+ packs,
188
+ contributions: contributions.map(({ packId, moduleId, contributionId, required, policyDigest }) => ({ packId, moduleId, contributionId, required, policyDigest })),
189
+ });
190
+ }
191
+
192
+ export function resolveOrganisationPolicy(blueprint) {
193
+ if (!blueprint || blueprint.schema !== 'ewai.resolved-organisation-blueprint/v1') {
194
+ throw new Error('A resolved Organisation Blueprint is required to resolve policy');
195
+ }
196
+ const packById = new Map(blueprint.packs.map((pack) => [pack.id, pack]));
197
+ const contributions = [];
198
+ const policyIds = new Set();
199
+ for (const { packId, module } of blueprint.modules) {
200
+ const pack = packById.get(packId);
201
+ if (!pack) throw new Error(`Policy contribution references unresolved Blueprint pack: ${packId}`);
202
+ for (const contribution of module.policies ?? []) {
203
+ const validated = validateOrganisationPolicy(contribution.content, {
204
+ publisherId: pack.blueprint.publisher.id,
205
+ contributionId: contribution.id,
206
+ });
207
+ if (policyIds.has(validated.policy.id)) {
208
+ throw new Error(`Ambiguous organisation policy id across selected Blueprint modules: ${validated.policy.id}`);
209
+ }
210
+ policyIds.add(validated.policy.id);
211
+ contributions.push({
212
+ packId,
213
+ moduleId: module.id,
214
+ contributionId: contribution.id,
215
+ required: module.required,
216
+ policyDigest: validated.digest,
217
+ policy: validated.policy,
218
+ });
219
+ }
220
+ }
221
+ contributions.sort((left, right) => left.policy.id.localeCompare(right.policy.id) || left.packId.localeCompare(right.packId));
222
+ const packs = blueprint.packs.map(packPin);
223
+ const effectiveDigest = contributions.length === 0 ? null : effectivePolicyDigest(blueprint.digest, packs, contributions);
224
+ return {
225
+ schema: 'ewai.resolved-organisation-policy/v1',
226
+ blueprintDigest: blueprint.digest,
227
+ packs,
228
+ contributions,
229
+ effectiveDigest,
230
+ };
231
+ }
232
+
233
+ export function previewPolicyBaseline(resolved) {
234
+ if (!resolved || resolved.contributions?.length === 0) {
235
+ return {
236
+ schema: 'ewai.policy-baseline-preview/v1',
237
+ status: 'not-configured',
238
+ enabled: false,
239
+ blocking: false,
240
+ notice: 'No organisation policy baseline has been selected for this project.',
241
+ counts: { policies: 0, rules: 0 },
242
+ policies: [],
243
+ };
244
+ }
245
+ if (resolved.schema !== 'ewai.resolved-organisation-policy/v1' || !resolved.effectiveDigest) {
246
+ throw new Error('A resolved organisation policy is required for preview');
247
+ }
248
+ return {
249
+ schema: 'ewai.policy-baseline-preview/v1',
250
+ status: 'available',
251
+ enabled: false,
252
+ blocking: false,
253
+ effectiveDigest: resolved.effectiveDigest,
254
+ counts: {
255
+ policies: resolved.contributions.length,
256
+ rules: resolved.contributions.reduce((count, contribution) => count + contribution.policy.rules.length, 0),
257
+ },
258
+ policies: resolved.contributions.map((contribution) => ({
259
+ id: contribution.policy.id,
260
+ title: contribution.policy.title,
261
+ version: contribution.policy.version,
262
+ publisher: { ...contribution.policy.publisher },
263
+ packId: contribution.packId,
264
+ moduleId: contribution.moduleId,
265
+ required: contribution.required,
266
+ policyDigest: contribution.policyDigest,
267
+ unmatchedOutcome: contribution.policy.unmatched_outcome,
268
+ ruleCount: contribution.policy.rules.length,
269
+ outcomes: [...new Set(contribution.policy.rules.map(({ outcome }) => outcome))].sort(),
270
+ reviewRoles: contribution.policy.review_roles.map(({ id, name }) => ({ id, name })),
271
+ })),
272
+ authority: {
273
+ advisoryUntilApproved: true,
274
+ designTimeOnly: true,
275
+ productionEnforcement: false,
276
+ },
277
+ };
278
+ }
279
+
280
+ function baselineStatus(selection, status, extra = {}) {
281
+ return {
282
+ schema: 'ewai.policy-baseline-status/v1',
283
+ status,
284
+ enabled: status !== 'not-configured' && status !== 'invalid',
285
+ blocking: ['invalid', 'stale'].includes(status),
286
+ ...(selection ? {
287
+ effectiveDigest: selection.effective_digest,
288
+ approvedBy: selection.approved_by,
289
+ approvedAt: selection.approved_at,
290
+ evidence: selection.evidence,
291
+ } : {}),
292
+ ...extra,
293
+ };
294
+ }
295
+
296
+ function atomicBaselineWrite(configPath, configContent, evidencePath, evidenceContent, testHooks = {}) {
297
+ mkdirSync(dirname(evidencePath), { recursive: true });
298
+ const suffix = `.tmp-${process.pid}-${randomUUID()}`;
299
+ const configTemp = `${configPath}${suffix}`;
300
+ const evidenceTemp = `${evidencePath}${suffix}`;
301
+ const priorConfig = readFileSync(configPath, 'utf8');
302
+ const hadEvidence = existsSync(evidencePath);
303
+ const priorEvidence = hadEvidence ? readFileSync(evidencePath, 'utf8') : null;
304
+ try {
305
+ writeFileSync(configTemp, configContent, { encoding: 'utf8', flag: 'wx' });
306
+ writeFileSync(evidenceTemp, evidenceContent, { encoding: 'utf8', flag: 'wx' });
307
+ renameSync(evidenceTemp, evidencePath);
308
+ if (testHooks.failAfterEvidence) throw new Error('Injected policy baseline failure');
309
+ renameSync(configTemp, configPath);
310
+ } catch (error) {
311
+ if (existsSync(configTemp)) unlinkSync(configTemp);
312
+ if (existsSync(evidenceTemp)) unlinkSync(evidenceTemp);
313
+ if (hadEvidence) writeFileSync(evidencePath, priorEvidence, 'utf8');
314
+ else if (existsSync(evidencePath)) unlinkSync(evidencePath);
315
+ if (readFileSync(configPath, 'utf8') !== priorConfig) writeFileSync(configPath, priorConfig, 'utf8');
316
+ throw error;
317
+ }
318
+ }
319
+
320
+ export function materialisePolicyBaseline(projectRoot, resolved, options = {}) {
321
+ if (options.confirmed !== true) throw new Error('Policy baseline materialisation requires explicit confirmation');
322
+ const approvedBy = String(options.approvedBy ?? '').trim();
323
+ if (!approvedBy) throw new Error('Policy baseline materialisation requires a named approver');
324
+ const expectedDigest = String(options.expectedDigest ?? '').trim();
325
+ if (!/^sha256:[a-f0-9]{64}$/.test(expectedDigest)) throw new Error('Policy baseline materialisation requires an exact expected digest');
326
+ if (!resolved || resolved.schema !== 'ewai.resolved-organisation-policy/v1' || resolved.contributions.length === 0 || !resolved.effectiveDigest) {
327
+ throw new Error('Policy baseline materialisation requires at least one resolved policy contribution');
328
+ }
329
+ if (resolved.effectiveDigest !== expectedDigest) {
330
+ throw new Error(`Policy baseline digest drift: expected ${expectedDigest}, resolved ${resolved.effectiveDigest}`);
331
+ }
332
+ const approvedAt = options.now ?? new Date().toISOString();
333
+ if (Number.isNaN(Date.parse(approvedAt))) throw new Error('Policy baseline approval time must be an ISO date-time');
334
+ const { config, paths } = loadProjectConfig(projectRoot);
335
+ const evidence = {
336
+ schema: 'ewai.organisation-policy-baseline/v1',
337
+ effectiveDigest: resolved.effectiveDigest,
338
+ blueprintDigest: resolved.blueprintDigest,
339
+ packs: resolved.packs,
340
+ contributions: resolved.contributions,
341
+ approval: { approvedBy, approvedAt },
342
+ authority: {
343
+ designTimeOnly: true,
344
+ productionEnforcement: false,
345
+ statement: 'EWAI checks the design, not production traffic.',
346
+ },
347
+ };
348
+ config.organisation_policy = {
349
+ status: 'enabled',
350
+ packs: resolved.packs.map(({ id, version, digest: packDigest, sourceClass }) => ({ id, version, digest: packDigest, source_class: sourceClass })),
351
+ blueprint_digest: resolved.blueprintDigest,
352
+ effective_digest: resolved.effectiveDigest,
353
+ approved_by: approvedBy,
354
+ approved_at: approvedAt,
355
+ evidence: ORGANISATION_POLICY_BASELINE_PATH,
356
+ };
357
+ const evidencePath = resolve(paths.projectRoot, ORGANISATION_POLICY_BASELINE_PATH);
358
+ atomicBaselineWrite(
359
+ paths.configPath,
360
+ YAML.stringify(config, { lineWidth: 0 }),
361
+ evidencePath,
362
+ `${JSON.stringify(evidence, null, 2)}\n`,
363
+ options.testHooks,
364
+ );
365
+ return readPolicyBaselineStatus(projectRoot);
366
+ }
367
+
368
+ export function readPolicyBaselineStatus(projectRoot, options = {}) {
369
+ const { config, paths } = loadProjectConfig(projectRoot);
370
+ const selection = config.organisation_policy;
371
+ if (!selection) {
372
+ return baselineStatus(null, 'not-configured', {
373
+ notice: 'No organisation policy baseline has been selected for this project.',
374
+ });
375
+ }
376
+ if (
377
+ selection.status !== 'enabled'
378
+ || selection.evidence !== ORGANISATION_POLICY_BASELINE_PATH
379
+ || !selection.effective_digest
380
+ || !selection.blueprint_digest
381
+ || !Array.isArray(selection.packs)
382
+ || selection.packs.length === 0
383
+ ) {
384
+ return baselineStatus(selection, 'invalid', { notice: 'The configured organisation policy baseline is incomplete.' });
385
+ }
386
+ const evidencePath = resolve(paths.projectRoot, selection.evidence);
387
+ if (!existsSync(evidencePath)) {
388
+ return baselineStatus(selection, 'invalid', { notice: 'The configured organisation policy evidence is missing.' });
389
+ }
390
+ let evidence;
391
+ try {
392
+ evidence = JSON.parse(readFileSync(evidencePath, 'utf8'));
393
+ } catch (error) {
394
+ return baselineStatus(selection, 'invalid', { notice: `The configured organisation policy evidence is invalid: ${error.message}` });
395
+ }
396
+ let evidenceIntegrity = false;
397
+ try {
398
+ if (
399
+ evidence.schema !== 'ewai.organisation-policy-baseline/v1'
400
+ || !Array.isArray(evidence.packs)
401
+ || !Array.isArray(evidence.contributions)
402
+ || evidence.contributions.length === 0
403
+ || evidence.blueprintDigest !== selection.blueprint_digest
404
+ || evidence.effectiveDigest !== selection.effective_digest
405
+ || evidence.approval?.approvedBy !== selection.approved_by
406
+ || evidence.approval?.approvedAt !== selection.approved_at
407
+ || evidence.authority?.designTimeOnly !== true
408
+ || evidence.authority?.productionEnforcement !== false
409
+ ) throw new Error('baseline envelope mismatch');
410
+ const selectedPins = selection.packs.map(({ id, version, digest: packDigest, source_class }) => ({ id, version, digest: packDigest, sourceClass: source_class }));
411
+ if (JSON.stringify(canonical(evidence.packs)) !== JSON.stringify(canonical(selectedPins))) throw new Error('pack pin mismatch');
412
+ for (const contribution of evidence.contributions) {
413
+ if (
414
+ !organisationPackId.safeParse(contribution.packId).success
415
+ || !slug.safeParse(contribution.moduleId).success
416
+ || !slug.safeParse(contribution.contributionId).success
417
+ || typeof contribution.required !== 'boolean'
418
+ ) throw new Error('invalid contribution identity');
419
+ const validated = validateOrganisationPolicy(contribution.policy, {
420
+ publisherId: contribution.packId.split('.')[1],
421
+ contributionId: contribution.contributionId,
422
+ });
423
+ if (validated.digest !== contribution.policyDigest) throw new Error('policy content digest mismatch');
424
+ }
425
+ if (effectivePolicyDigest(evidence.blueprintDigest, evidence.packs, evidence.contributions) !== evidence.effectiveDigest) {
426
+ throw new Error('effective digest mismatch');
427
+ }
428
+ evidenceIntegrity = true;
429
+ } catch {
430
+ evidenceIntegrity = false;
431
+ }
432
+ if (!evidenceIntegrity) {
433
+ return baselineStatus(selection, 'invalid', { notice: 'The configured organisation policy evidence does not match its approved pin.' });
434
+ }
435
+ if (options.resolved && options.resolved.effectiveDigest !== selection.effective_digest) {
436
+ return baselineStatus(selection, 'stale', {
437
+ acceptedDigest: selection.effective_digest,
438
+ currentDigest: options.resolved.effectiveDigest,
439
+ notice: 'Installed policy content has changed since approval. Re-resolve it and obtain a new named approval.',
440
+ });
441
+ }
442
+ return baselineStatus(selection, 'enabled', {
443
+ blocking: false,
444
+ policyCount: evidence.contributions.length,
445
+ ruleCount: evidence.contributions.reduce((count, contribution) => count + contribution.policy.rules.length, 0),
446
+ authority: { designTimeOnly: true, productionEnforcement: false },
447
+ });
448
+ }
package/src/packs.mjs ADDED
@@ -0,0 +1,44 @@
1
+ import { existsSync, readdirSync, readFileSync } from 'node:fs';
2
+ import { homedir } from 'node:os';
3
+ import { dirname, resolve } from 'node:path';
4
+ import { fileURLToPath } from 'node:url';
5
+ import YAML from 'yaml';
6
+
7
+ const moduleDir = dirname(fileURLToPath(import.meta.url));
8
+ export const defaultPackRoot = resolve(moduleDir, '../packs');
9
+
10
+ function findPackFiles(root) {
11
+ if (!existsSync(root)) return [];
12
+ const found = [];
13
+ for (const entry of readdirSync(root, { withFileTypes: true })) {
14
+ const path = resolve(root, entry.name);
15
+ if (entry.isDirectory()) found.push(...findPackFiles(path));
16
+ if (entry.isFile() && entry.name === 'pack.yaml') found.push(path);
17
+ }
18
+ return found;
19
+ }
20
+
21
+ export function listPacks(root = defaultPackRoot) {
22
+ return findPackFiles(root).map((path) => {
23
+ const pack = YAML.parse(readFileSync(path, 'utf8'));
24
+ return { ...pack, path };
25
+ }).sort((left, right) => left.id.localeCompare(right.id));
26
+ }
27
+
28
+ export function projectPackRoots(projectRoot, options = {}) {
29
+ return [
30
+ defaultPackRoot,
31
+ resolve(options.home ?? homedir(), '.ewai/packs'),
32
+ resolve(projectRoot, '.ewai-pipeline/packs')
33
+ ];
34
+ }
35
+
36
+ export function listProjectPacks(projectRoot, options = {}) {
37
+ const packs = (options.roots ?? projectPackRoots(projectRoot, options)).flatMap((root) => listPacks(root));
38
+ const byId = new Map();
39
+ for (const pack of packs) {
40
+ if (byId.has(pack.id)) throw new Error(`Duplicate EWAI pack id across installed roots: ${pack.id}`);
41
+ byId.set(pack.id, pack);
42
+ }
43
+ return [...byId.values()].sort((left, right) => left.id.localeCompare(right.id));
44
+ }
package/src/paths.mjs ADDED
@@ -0,0 +1,62 @@
1
+ import { existsSync, readFileSync, realpathSync } from 'node:fs';
2
+ import { dirname, isAbsolute, parse, relative, resolve } from 'node:path';
3
+
4
+ export const PROJECT_MARKERS = [
5
+ 'SPECS/pipeline.yaml',
6
+ '.ewai-pipeline/project.json',
7
+ '.git'
8
+ ];
9
+
10
+ export function resolveProjectRoot(start = process.cwd()) {
11
+ let current = realpathSync(resolve(start));
12
+ const filesystemRoot = parse(current).root;
13
+ let nearestGit = '';
14
+
15
+ while (true) {
16
+ if (existsSync(resolve(current, '.ewai-pipeline/project.json')) || existsSync(resolve(current, 'SPECS/pipeline.yaml'))) return current;
17
+ if (!nearestGit && existsSync(resolve(current, '.git'))) nearestGit = current;
18
+ if (current === filesystemRoot) break;
19
+ current = dirname(current);
20
+ }
21
+
22
+ if (nearestGit) return nearestGit;
23
+
24
+ throw new Error(`No EWAI project or Git repository found from ${resolve(start)}`);
25
+ }
26
+
27
+ function within(root, target) {
28
+ const path = relative(root, target);
29
+ return path === '' || (!path.startsWith('..') && !isAbsolute(path));
30
+ }
31
+
32
+ function runtimeSpecsRoot(root, runtimeProjectPath) {
33
+ if (!existsSync(runtimeProjectPath)) return '';
34
+ try {
35
+ const runtime = JSON.parse(readFileSync(runtimeProjectPath, 'utf8'));
36
+ return String(runtime.specsRoot ?? '').trim();
37
+ } catch {
38
+ return '';
39
+ }
40
+ }
41
+
42
+ export function projectPaths(projectRoot, options = {}) {
43
+ const root = resolve(projectRoot);
44
+ const runtimeRoot = resolve(root, '.ewai-pipeline');
45
+ const runtimeProjectPath = resolve(runtimeRoot, 'project.json');
46
+ const configured = String(options.specsRoot ?? runtimeSpecsRoot(root, runtimeProjectPath) ?? '').trim() || 'SPECS';
47
+ const specsRoot = isAbsolute(configured) ? resolve(configured) : resolve(root, configured);
48
+ if (!within(root, specsRoot)) throw new Error(`SPECS root must stay inside the EWAI workspace: ${configured}`);
49
+ const specsRelative = relative(root, specsRoot).replaceAll('\\', '/') || '.';
50
+
51
+ return {
52
+ projectRoot: root,
53
+ specsRoot,
54
+ specsRelative,
55
+ configPath: resolve(specsRoot, 'pipeline.yaml'),
56
+ discoveryPath: resolve(specsRoot, '2.Purpose/explorations/project-discovery.md'),
57
+ personaRoot: resolve(specsRoot, '1.Scope/personas'),
58
+ buildRoot: resolve(specsRoot, '6.Build'),
59
+ runtimeRoot,
60
+ runtimeProjectPath
61
+ };
62
+ }