@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,623 @@
1
+ import { createHash, randomUUID } from 'node:crypto';
2
+ import { existsSync, mkdirSync, readFileSync, readdirSync, renameSync, rmSync, writeFileSync } from 'node:fs';
3
+ import { dirname, isAbsolute, relative, resolve } from 'node:path';
4
+ import { z } from 'zod';
5
+ import { atomicJson } from './delivery-documents.mjs';
6
+ import { ORGANISATION_POLICY_BASELINE_PATH, readPolicyBaselineStatus } from './organisation-policies.mjs';
7
+ import { loadProjectConfig } from './project.mjs';
8
+
9
+ const dimensions = ['resource', 'operation', 'data_entity', 'data_classification', 'actor', 'environment', 'destination', 'model', 'ai_use', 'retention'];
10
+ const outcomes = ['allow', 'deny', 'review-required', 'allow-with-controls', 'unassessed'];
11
+ const outcomeRank = new Map(['allow', 'allow-with-controls', 'unassessed', 'review-required', 'deny'].map((outcome, index) => [outcome, index]));
12
+ const slug = z.string().regex(/^[a-z0-9]+(?:-[a-z0-9]+)*$/);
13
+ const intentReference = z.string().regex(/^[a-z0-9]+(?:-[a-z0-9]+)*\/[a-z0-9]+(?:-[a-z0-9]+)*$/);
14
+ const digestSchema = z.string().regex(/^sha256:[a-f0-9]{64}$/);
15
+ const valueId = z.string().min(1).max(120).regex(/^[a-z0-9]+(?:[.-][a-z0-9]+)*$/);
16
+ const boundedText = (maximum) => z.string().trim().min(1).max(maximum);
17
+ const unique = (values) => new Set(values).size === values.length;
18
+ const factValues = z.array(valueId).min(1).max(40).refine(unique, 'must not contain duplicates');
19
+ const evidenceReference = z.string().trim().min(1).max(500).refine((value) => (
20
+ !isAbsolute(value) && !value.includes('\0') && !value.split(/[\\/]+/).includes('..')
21
+ ), 'must be a bounded project evidence reference');
22
+
23
+ const personaSourceSchema = z.object({
24
+ id: z.string().min(1).max(160),
25
+ name: boundedText(160),
26
+ tier: z.enum(['core', 'personal', 'project', 'premium']),
27
+ });
28
+
29
+ const sourceSchema = z.object({
30
+ kind: z.enum(['observed', 'model-proposal', 'persona-hypothesis']),
31
+ reference: boundedText(500),
32
+ persona: personaSourceSchema.optional(),
33
+ }).strict().superRefine((source, context) => {
34
+ if (source.kind === 'persona-hypothesis' && !source.persona) {
35
+ context.addIssue({ code: 'custom', path: ['persona'], message: 'persona hypotheses require safe persona provenance' });
36
+ }
37
+ if (source.kind !== 'persona-hypothesis' && source.persona) {
38
+ context.addIssue({ code: 'custom', path: ['persona'], message: 'persona provenance applies only to persona hypotheses' });
39
+ }
40
+ });
41
+
42
+ const factSchema = z.object({
43
+ id: slug,
44
+ dimension: z.enum(dimensions),
45
+ values: factValues,
46
+ rationale: boundedText(1200),
47
+ source: sourceSchema,
48
+ }).strict();
49
+
50
+ const intentSchema = z.object({
51
+ reference: intentReference,
52
+ revision: z.number().int().positive(),
53
+ digest: digestSchema,
54
+ }).strict();
55
+
56
+ const decisionSchema = z.object({
57
+ factId: slug,
58
+ disposition: z.enum(['confirmed', 'rejected']),
59
+ values: factValues.optional(),
60
+ }).strict().superRefine((decision, context) => {
61
+ if (decision.disposition === 'rejected' && decision.values) {
62
+ context.addIssue({ code: 'custom', path: ['values'], message: 'rejected facts may not provide replacement values' });
63
+ }
64
+ });
65
+
66
+ const scopeSchema = z.object(Object.fromEntries(dimensions.map((dimension) => [dimension, factValues.optional()])))
67
+ .strict()
68
+ .refine((value) => Object.keys(value).length > 0, 'exception scope must contain at least one bounded condition');
69
+
70
+ function canonical(value) {
71
+ if (Array.isArray(value)) return value.map(canonical);
72
+ if (value && typeof value === 'object') {
73
+ return Object.fromEntries(Object.keys(value).sort().map((key) => [key, canonical(value[key])]));
74
+ }
75
+ return value;
76
+ }
77
+
78
+ function sha(value) {
79
+ return `sha256:${createHash('sha256').update(typeof value === 'string' ? value : JSON.stringify(canonical(value))).digest('hex')}`;
80
+ }
81
+
82
+ function parseWith(schema, value, label) {
83
+ try {
84
+ return schema.parse(value);
85
+ } catch (error) {
86
+ const details = error.issues?.map((issue) => `${issue.path.join('.') || label} ${issue.message}`).join('; ') ?? error.message;
87
+ throw new Error(`${label} is invalid: ${details}`);
88
+ }
89
+ }
90
+
91
+ function cleanHuman(value, label) {
92
+ const result = String(value ?? '').trim();
93
+ if (!result || result.length > 160 || /[\u0000-\u001f\u007f]/.test(result)) throw new Error(`${label} requires a named ${label.toLowerCase()}`);
94
+ return result;
95
+ }
96
+
97
+ function requireHumanAuthority(input) {
98
+ if (input.authority !== 'human') throw new Error('This decision requires named human authority; a model or persona cannot make it');
99
+ }
100
+
101
+ function referenceParts(reference) {
102
+ const checked = intentReference.parse(reference);
103
+ return checked.split('/');
104
+ }
105
+
106
+ function policyPaths(projectRoot, reference) {
107
+ const { config, paths } = loadProjectConfig(projectRoot);
108
+ const [domain, slugValue] = referenceParts(reference);
109
+ return {
110
+ config,
111
+ paths,
112
+ proposalPath: resolve(paths.runtimeRoot, 'policy/facts', domain, `${slugValue}.draft.json`),
113
+ factsPath: resolve(paths.specsRoot, '3.Evidence/policy/facts', domain, `${slugValue}.json`),
114
+ factsMarkdownPath: resolve(paths.specsRoot, '3.Evidence/policy/facts', domain, `${slugValue}.md`),
115
+ evaluationPath: resolve(paths.specsRoot, '3.Evidence/policy/evaluations', domain, `${slugValue}.json`),
116
+ evaluationMarkdownPath: resolve(paths.specsRoot, '3.Evidence/policy/evaluations', domain, `${slugValue}.md`),
117
+ reviewsRoot: resolve(paths.specsRoot, '3.Evidence/policy/reviews', domain, slugValue),
118
+ exceptionsRoot: resolve(paths.specsRoot, '3.Evidence/policy/exceptions', domain, slugValue),
119
+ };
120
+ }
121
+
122
+ function readJson(path, label) {
123
+ if (!existsSync(path)) throw new Error(`${label} does not exist`);
124
+ try {
125
+ return JSON.parse(readFileSync(path, 'utf8'));
126
+ } catch (error) {
127
+ throw new Error(`${label} is invalid JSON: ${error.message}`);
128
+ }
129
+ }
130
+
131
+ function approvedBaseline(projectRoot) {
132
+ const status = readPolicyBaselineStatus(projectRoot);
133
+ if (status.status !== 'enabled') throw new Error(`An approved current organisation policy baseline is required; current status is ${status.status}`);
134
+ const { paths } = loadProjectConfig(projectRoot);
135
+ const baseline = readJson(resolve(paths.projectRoot, ORGANISATION_POLICY_BASELINE_PATH), 'Organisation policy baseline');
136
+ return { status, baseline };
137
+ }
138
+
139
+ function atomicPair(jsonPath, jsonContent, markdownPath, markdownContent) {
140
+ mkdirSync(dirname(jsonPath), { recursive: true });
141
+ mkdirSync(dirname(markdownPath), { recursive: true });
142
+ const suffix = `.ewai-${process.pid}-${randomUUID()}.tmp`;
143
+ const jsonTemp = `${jsonPath}${suffix}`;
144
+ const markdownTemp = `${markdownPath}${suffix}`;
145
+ const priorJson = existsSync(jsonPath) ? readFileSync(jsonPath, 'utf8') : null;
146
+ const priorMarkdown = existsSync(markdownPath) ? readFileSync(markdownPath, 'utf8') : null;
147
+ try {
148
+ writeFileSync(jsonTemp, jsonContent, { encoding: 'utf8', flag: 'wx' });
149
+ writeFileSync(markdownTemp, markdownContent, { encoding: 'utf8', flag: 'wx' });
150
+ renameSync(markdownTemp, markdownPath);
151
+ try {
152
+ renameSync(jsonTemp, jsonPath);
153
+ } catch (error) {
154
+ if (priorMarkdown == null) rmSync(markdownPath, { force: true });
155
+ else writeFileSync(markdownPath, priorMarkdown, 'utf8');
156
+ throw error;
157
+ }
158
+ } catch (error) {
159
+ rmSync(jsonTemp, { force: true });
160
+ rmSync(markdownTemp, { force: true });
161
+ if (priorJson != null && (!existsSync(jsonPath) || readFileSync(jsonPath, 'utf8') !== priorJson)) writeFileSync(jsonPath, priorJson, 'utf8');
162
+ throw error;
163
+ }
164
+ }
165
+
166
+ function factProposalMarkdown(proposal) {
167
+ const rows = proposal.facts.map((fact) => `| \`${fact.id}\` | ${fact.dimension} | ${fact.values.join(', ')} | ${fact.provenance.kind} |`).join('\n');
168
+ return `# Confirmed policy design facts — ${proposal.intent.reference}\n\n` +
169
+ `**Intent revision:** ${proposal.intent.revision} \n**Policy digest:** \`${proposal.policyDigest}\` \n` +
170
+ `**Facts digest:** \`${proposal.factsDigest}\` \n**Confirmed by:** ${proposal.confirmation.confirmedBy} \n` +
171
+ `**Confirmed at:** ${proposal.confirmation.confirmedAt}\n\n| Fact | Dimension | Values | Original source |\n|---|---|---|---|\n${rows}\n\n` +
172
+ `Only the named confirmation makes these facts authoritative. Model and persona provenance remains advisory evidence.\n`;
173
+ }
174
+
175
+ function evaluationMarkdown(evaluation) {
176
+ const rows = evaluation.matchedRules.map((rule) => `| \`${rule.qualifiedRuleId}\` | ${rule.title} | ${rule.outcome} |`).join('\n') || '| — | No rule matched | — |';
177
+ return `# Policy design evaluation — ${evaluation.intent.reference}\n\n` +
178
+ `**Outcome:** ${evaluation.governingOutcome} \n**Evaluation digest:** \`${evaluation.evaluationDigest}\` \n` +
179
+ `**Policy digest:** \`${evaluation.policyDigest}\` \n**Facts digest:** \`${evaluation.factsDigest}\`\n\n` +
180
+ `| Rule | Title | Outcome |\n|---|---|---|\n${rows}\n\n` +
181
+ `EWAI checks the design, not production traffic. This result is not production enforcement, certification, residual-risk acceptance or release authority.\n`;
182
+ }
183
+
184
+ export function preparePolicyFacts(projectRoot, input = {}, options = {}) {
185
+ const { status } = approvedBaseline(projectRoot);
186
+ const intent = parseWith(intentSchema, input.intent, 'Policy fact intent');
187
+ if (!Array.isArray(input.facts) || input.facts.length === 0 || input.facts.length > 200) throw new Error('Policy fact proposal requires between 1 and 200 facts');
188
+ const facts = input.facts.map((fact) => parseWith(factSchema, fact, 'Policy fact'));
189
+ if (!unique(facts.map(({ id }) => id))) throw new Error('Policy fact proposal contains a duplicate fact id');
190
+ const paths = policyPaths(projectRoot, intent.reference);
191
+ const existing = existsSync(paths.proposalPath) ? readJson(paths.proposalPath, 'Policy fact proposal') : null;
192
+ const revision = Number(existing?.revision ?? 0) + 1;
193
+ const proposalDigest = sha({ intent, policyDigest: status.effectiveDigest, facts });
194
+ const proposal = {
195
+ schema: 'ewai.policy-fact-proposal/v1',
196
+ revision,
197
+ preparedAt: options.now ?? new Date().toISOString(),
198
+ intent,
199
+ policyDigest: status.effectiveDigest,
200
+ proposalDigest,
201
+ facts,
202
+ authority: { authoritative: false, requiresNamedHumanConfirmation: true },
203
+ };
204
+ atomicJson(paths.proposalPath, proposal);
205
+ return proposal;
206
+ }
207
+
208
+ function factsDigest(record) {
209
+ return sha({
210
+ intent: record.intent,
211
+ policyDigest: record.policyDigest,
212
+ facts: record.facts,
213
+ });
214
+ }
215
+
216
+ function validateConfirmedFacts(record) {
217
+ if (record?.schema !== 'ewai.confirmed-policy-facts/v1' || record.authority?.authoritative !== true) throw new Error('Confirmed policy facts are invalid or non-authoritative');
218
+ parseWith(intentSchema, record.intent, 'Confirmed fact intent');
219
+ digestSchema.parse(record.policyDigest);
220
+ digestSchema.parse(record.proposalDigest);
221
+ const facts = record.facts.map((fact) => parseWith(factSchema, {
222
+ id: fact.id,
223
+ dimension: fact.dimension,
224
+ values: fact.values,
225
+ rationale: fact.rationale,
226
+ source: fact.provenance,
227
+ }, 'Confirmed fact'));
228
+ const computed = factsDigest({ intent: record.intent, policyDigest: record.policyDigest, facts: record.facts });
229
+ if (computed !== record.factsDigest) throw new Error('Confirmed policy facts do not match their accepted digest');
230
+ return record;
231
+ }
232
+
233
+ export function confirmPolicyFacts(projectRoot, input = {}, options = {}) {
234
+ if (input.confirmed !== true) throw new Error('Policy fact confirmation requires explicit confirmation');
235
+ requireHumanAuthority(input);
236
+ const confirmedBy = cleanHuman(input.confirmedBy, 'Named confirmer');
237
+ const reference = intentReference.parse(input.intentReference);
238
+ const paths = policyPaths(projectRoot, reference);
239
+ const proposal = readJson(paths.proposalPath, 'Policy fact proposal');
240
+ if (Number(input.expectedRevision) !== proposal.revision) throw new Error(`A newer revision of this policy fact proposal exists (current revision ${proposal.revision})`);
241
+ if (input.expectedProposalDigest !== proposal.proposalDigest) throw new Error('Policy fact proposal digest drift; review the current proposal before confirmation');
242
+ if (!Array.isArray(input.decisions)) throw new Error('Policy fact confirmation requires a decision for every proposed fact');
243
+ const decisions = input.decisions.map((decision) => parseWith(decisionSchema, decision, 'Policy fact decision'));
244
+ if (!unique(decisions.map(({ factId }) => factId)) || decisions.length !== proposal.facts.length) throw new Error('Policy fact confirmation requires exactly one decision for every proposed fact');
245
+ const byId = new Map(decisions.map((decision) => [decision.factId, decision]));
246
+ if (proposal.facts.some(({ id }) => !byId.has(id)) || decisions.some(({ factId }) => !proposal.facts.some(({ id }) => id === factId))) {
247
+ throw new Error('Policy fact confirmation requires exactly one decision for every proposed fact');
248
+ }
249
+ const facts = proposal.facts.flatMap((fact) => {
250
+ const decision = byId.get(fact.id);
251
+ if (decision.disposition === 'rejected') return [];
252
+ return [{
253
+ id: fact.id,
254
+ dimension: fact.dimension,
255
+ values: decision.values ?? fact.values,
256
+ rationale: fact.rationale,
257
+ provenance: fact.source,
258
+ }];
259
+ });
260
+ if (facts.length === 0) throw new Error('At least one policy design fact must be confirmed');
261
+ const existing = existsSync(paths.factsPath) ? readJson(paths.factsPath, 'Confirmed policy facts') : null;
262
+ const record = {
263
+ schema: 'ewai.confirmed-policy-facts/v1',
264
+ revision: Number(existing?.revision ?? 0) + 1,
265
+ intent: proposal.intent,
266
+ policyDigest: proposal.policyDigest,
267
+ proposalDigest: proposal.proposalDigest,
268
+ facts,
269
+ confirmation: { confirmedBy, confirmedAt: options.now ?? new Date().toISOString(), decisions },
270
+ authority: { authoritative: true, source: 'named-human-confirmation', modelAuthority: false, personaAuthority: false },
271
+ };
272
+ record.factsDigest = factsDigest(record);
273
+ atomicPair(paths.factsPath, `${JSON.stringify(record, null, 2)}\n`, paths.factsMarkdownPath, factProposalMarkdown(record));
274
+ return record;
275
+ }
276
+
277
+ function conditionMatches(when, factsByDimension) {
278
+ const matched = [];
279
+ for (const [dimension, expected] of Object.entries(when)) {
280
+ const facts = factsByDimension.get(dimension) ?? [];
281
+ const relevant = facts.filter((fact) => fact.values.some((value) => expected.includes(value)));
282
+ if (relevant.length === 0) return null;
283
+ matched.push(...relevant.map((fact) => ({ id: fact.id, dimension: fact.dimension, values: fact.values.filter((value) => expected.includes(value)) })));
284
+ }
285
+ return matched.sort((left, right) => left.dimension.localeCompare(right.dimension) || left.id.localeCompare(right.id));
286
+ }
287
+
288
+ function governingOutcome(values) {
289
+ return [...values].sort((left, right) => (outcomeRank.get(right) ?? -1) - (outcomeRank.get(left) ?? -1))[0] ?? 'unassessed';
290
+ }
291
+
292
+ function currentFacts(projectRoot, reference) {
293
+ const paths = policyPaths(projectRoot, reference);
294
+ return validateConfirmedFacts(readJson(paths.factsPath, 'Confirmed policy facts'));
295
+ }
296
+
297
+ function evaluationCore(baseline, facts) {
298
+ const factsByDimension = new Map(dimensions.map((dimension) => [dimension, facts.facts.filter((fact) => fact.dimension === dimension)]));
299
+ const matchedRules = [];
300
+ const unmatchedPolicies = [];
301
+ for (const contribution of baseline.contributions) {
302
+ let policyMatches = 0;
303
+ for (const rule of contribution.policy.rules) {
304
+ const matchedFacts = conditionMatches(rule.when, factsByDimension);
305
+ if (!matchedFacts) continue;
306
+ policyMatches += 1;
307
+ matchedRules.push({
308
+ policyId: contribution.policy.id,
309
+ ruleId: rule.id,
310
+ qualifiedRuleId: `${contribution.policy.id}:${rule.id}`,
311
+ title: rule.title,
312
+ outcome: rule.outcome,
313
+ matchedFacts,
314
+ reviewRole: rule.review_role ?? null,
315
+ controls: rule.controls,
316
+ exceptions: rule.exceptions,
317
+ conditions: rule.when,
318
+ });
319
+ }
320
+ if (policyMatches === 0) unmatchedPolicies.push({
321
+ policyId: contribution.policy.id,
322
+ outcome: contribution.policy.unmatched_outcome,
323
+ });
324
+ }
325
+ matchedRules.sort((left, right) => left.qualifiedRuleId.localeCompare(right.qualifiedRuleId));
326
+ unmatchedPolicies.sort((left, right) => left.policyId.localeCompare(right.policyId));
327
+ const consideredOutcomes = [...matchedRules.map(({ outcome }) => outcome), ...unmatchedPolicies.map(({ outcome }) => outcome)];
328
+ return {
329
+ governingOutcome: governingOutcome(consideredOutcomes),
330
+ consideredOutcomes: [...new Set(consideredOutcomes)].sort((left, right) => (outcomeRank.get(right) ?? -1) - (outcomeRank.get(left) ?? -1)),
331
+ matchedRules,
332
+ unmatchedPolicies,
333
+ controls: matchedRules.flatMap((rule) => rule.controls.map((control) => ({ ...control, ruleId: rule.qualifiedRuleId }))),
334
+ requiredReviews: matchedRules.filter(({ outcome }) => outcome === 'review-required').map((rule) => ({ ruleId: rule.qualifiedRuleId, reviewRole: rule.reviewRole })),
335
+ };
336
+ }
337
+
338
+ export function evaluatePolicyDesign(projectRoot, input = {}, options = {}) {
339
+ const reference = intentReference.parse(input.intentReference);
340
+ const { status, baseline } = approvedBaseline(projectRoot);
341
+ const facts = currentFacts(projectRoot, reference);
342
+ if (input.expectedPolicyDigest !== status.effectiveDigest || facts.policyDigest !== status.effectiveDigest) throw new Error('Policy evaluation requires the current approved policy digest');
343
+ if (input.expectedFactsDigest !== facts.factsDigest) throw new Error('Policy evaluation facts are stale; review the current confirmed facts');
344
+ if (Number(input.expectedIntentRevision) !== facts.intent.revision) throw new Error('Policy evaluation intent revision is stale');
345
+ const core = evaluationCore(baseline, facts);
346
+ const paths = policyPaths(projectRoot, reference);
347
+ const existing = existsSync(paths.evaluationPath) ? currentEvaluation(projectRoot, reference) : null;
348
+ const record = {
349
+ schema: 'ewai.policy-design-evaluation/v1',
350
+ revision: Number(existing?.revision ?? 0) + 1,
351
+ intent: facts.intent,
352
+ policyDigest: status.effectiveDigest,
353
+ factsDigest: facts.factsDigest,
354
+ ...core,
355
+ limitations: [
356
+ 'Design-time evidence only; production enforcement remains outside EWAI.',
357
+ 'Policy correctness, compliance certification and residual-risk acceptance require accountable human assurance.',
358
+ ],
359
+ authority: { designTimeOnly: true, productionEnforcement: false, buildApproved: false, releaseApproved: false },
360
+ };
361
+ const comparable = { ...record };
362
+ delete comparable.revision;
363
+ record.evaluationDigest = sha(comparable);
364
+ if (existing?.evaluationDigest === record.evaluationDigest) return existing;
365
+ if (options.persist !== false) {
366
+ atomicPair(paths.evaluationPath, `${JSON.stringify(record, null, 2)}\n`, paths.evaluationMarkdownPath, evaluationMarkdown(record));
367
+ }
368
+ return record;
369
+ }
370
+
371
+ function currentEvaluation(projectRoot, reference) {
372
+ const record = readJson(policyPaths(projectRoot, reference).evaluationPath, 'Policy design evaluation');
373
+ if (record.schema !== 'ewai.policy-design-evaluation/v1') throw new Error('Policy design evaluation has an unsupported schema');
374
+ const comparable = { ...record };
375
+ delete comparable.revision;
376
+ delete comparable.evaluationDigest;
377
+ if (sha(comparable) !== record.evaluationDigest) throw new Error('Policy design evaluation does not match its accepted digest');
378
+ return record;
379
+ }
380
+
381
+ function assertEvaluationCurrent(projectRoot, evaluation) {
382
+ const baseline = readPolicyBaselineStatus(projectRoot);
383
+ if (baseline.status !== 'enabled' || baseline.effectiveDigest !== evaluation.policyDigest) {
384
+ throw new Error('Policy decision references a stale evaluation because the approved policy baseline changed');
385
+ }
386
+ let facts;
387
+ try {
388
+ facts = currentFacts(projectRoot, evaluation.intent.reference);
389
+ } catch (error) {
390
+ throw new Error(`Policy decision references a stale evaluation because confirmed facts are invalid: ${error.message}`);
391
+ }
392
+ if (
393
+ facts.factsDigest !== evaluation.factsDigest
394
+ || facts.intent.revision !== evaluation.intent.revision
395
+ || facts.intent.digest !== evaluation.intent.digest
396
+ ) {
397
+ throw new Error('Policy decision references a stale evaluation because confirmed facts or intent revision changed');
398
+ }
399
+ return true;
400
+ }
401
+
402
+ function safeDecisionFile(root, ruleId, decisionDigest) {
403
+ const name = `${ruleId.replace(/[^a-z0-9.-]+/gi, '-')}-${decisionDigest.slice(7, 23)}.json`;
404
+ const path = resolve(root, name);
405
+ const rel = relative(root, path);
406
+ if (!rel || rel.startsWith('..') || isAbsolute(rel)) throw new Error('Unsafe policy decision destination');
407
+ return path;
408
+ }
409
+
410
+ function persistImmutableDecision(root, ruleId, decisionDigest, record) {
411
+ mkdirSync(root, { recursive: true });
412
+ const existing = decisionRecords(root, record.schema).filter((candidate) => (
413
+ candidate.evaluationDigest === record.evaluationDigest && candidate.ruleId === record.ruleId
414
+ ));
415
+ const identical = existing.find((candidate) => candidate.decisionDigest === decisionDigest);
416
+ if (identical) return identical;
417
+ if (existing.length > 0) {
418
+ throw new Error(`Policy rule ${record.ruleId} already has a current decision; create a new evaluation before replacing it`);
419
+ }
420
+ const path = safeDecisionFile(root, ruleId, decisionDigest);
421
+ if (existsSync(path)) return readJson(path, 'Policy decision');
422
+ atomicJson(path, record);
423
+ return record;
424
+ }
425
+
426
+ function matchedRule(evaluation, qualifiedRuleId) {
427
+ const rule = evaluation.matchedRules.find((candidate) => candidate.qualifiedRuleId === qualifiedRuleId);
428
+ if (!rule) throw new Error(`Policy rule is not matched by this evaluation: ${qualifiedRuleId}`);
429
+ return rule;
430
+ }
431
+
432
+ export function recordPolicyReview(projectRoot, input = {}, options = {}) {
433
+ requireHumanAuthority(input);
434
+ const reviewedBy = cleanHuman(input.reviewedBy, 'Named reviewer');
435
+ const reviewerRole = slug.parse(input.reviewerRole);
436
+ const reference = intentReference.parse(input.intentReference);
437
+ const evaluation = currentEvaluation(projectRoot, reference);
438
+ assertEvaluationCurrent(projectRoot, evaluation);
439
+ if (input.evaluationDigest !== evaluation.evaluationDigest) throw new Error('Policy review references a stale evaluation');
440
+ const rule = matchedRule(evaluation, input.ruleId);
441
+ if (rule.outcome !== 'review-required') throw new Error('Policy review applies only to a review-required rule');
442
+ if (reviewerRole !== rule.reviewRole) throw new Error(`Policy review requires the required review role: ${rule.reviewRole}`);
443
+ const decision = z.enum(['allow', 'deny', 'allow-with-controls']).parse(input.decision);
444
+ const rationale = boundedText(2000).parse(input.rationale);
445
+ const evidence = z.array(evidenceReference).min(1).max(40).parse(input.evidence);
446
+ const controls = z.array(boundedText(500)).max(40).default([]).parse(input.controls ?? []);
447
+ if (decision === 'allow-with-controls' && controls.length === 0) throw new Error('An allow-with-controls review requires explicit controls');
448
+ const decisionDigest = sha({ evaluationDigest: evaluation.evaluationDigest, ruleId: rule.qualifiedRuleId, reviewerRole, reviewedBy, decision, rationale, evidence, controls });
449
+ const record = {
450
+ schema: 'ewai.policy-review/v1', decisionDigest,
451
+ evaluationDigest: evaluation.evaluationDigest, intentReference: reference,
452
+ ruleId: rule.qualifiedRuleId, reviewerRole, reviewedBy, decision, rationale, evidence, controls,
453
+ reviewedAt: options.now ?? new Date().toISOString(),
454
+ authority: { namedHumanDecision: true, buildApproved: false, releaseApproved: false },
455
+ };
456
+ const paths = policyPaths(projectRoot, reference);
457
+ if (decisionRecords(paths.exceptionsRoot, 'ewai.policy-exception/v1').some((candidate) => (
458
+ candidate.evaluationDigest === evaluation.evaluationDigest && candidate.ruleId === rule.qualifiedRuleId
459
+ ))) throw new Error(`Policy rule ${rule.qualifiedRuleId} already has a current decision; create a new evaluation before replacing it`);
460
+ return persistImmutableDecision(paths.reviewsRoot, rule.qualifiedRuleId, decisionDigest, record);
461
+ }
462
+
463
+ function scopeBoundedByRule(scope, rule, facts) {
464
+ const factsByDimension = new Map(dimensions.map((dimension) => [dimension, new Set(facts.facts.filter((fact) => fact.dimension === dimension).flatMap(({ values }) => values))]));
465
+ for (const [dimension, values] of Object.entries(scope)) {
466
+ if (!rule.conditions[dimension]) return false;
467
+ if (values.some((value) => !rule.conditions[dimension].includes(value) || !factsByDimension.get(dimension).has(value))) return false;
468
+ }
469
+ return Object.keys(rule.conditions).every((dimension) => scope[dimension]?.length > 0);
470
+ }
471
+
472
+ export function recordPolicyException(projectRoot, input = {}, options = {}) {
473
+ requireHumanAuthority(input);
474
+ const approvedBy = cleanHuman(input.approvedBy, 'Named exception approver');
475
+ const owner = cleanHuman(input.owner, 'Exception owner');
476
+ const reviewerRole = slug.parse(input.reviewerRole);
477
+ const reference = intentReference.parse(input.intentReference);
478
+ const evaluation = currentEvaluation(projectRoot, reference);
479
+ assertEvaluationCurrent(projectRoot, evaluation);
480
+ if (input.evaluationDigest !== evaluation.evaluationDigest) throw new Error('Policy exception references a stale evaluation');
481
+ const rule = matchedRule(evaluation, input.ruleId);
482
+ if (!rule.exceptions.permitted) throw new Error(`Policy rule ${rule.qualifiedRuleId} does not permit exceptions`);
483
+ if (reviewerRole !== rule.exceptions.review_role) throw new Error(`Policy exception requires review role ${rule.exceptions.review_role}`);
484
+ const scope = parseWith(scopeSchema, input.scope, 'Policy exception scope');
485
+ const facts = currentFacts(projectRoot, reference);
486
+ if (!scopeBoundedByRule(scope, rule, facts)) throw new Error('Policy exception scope must be bounded by the matched rule and confirmed facts');
487
+ const rationale = boundedText(2000).parse(input.rationale);
488
+ const compensatingControls = parseWith(z.array(boundedText(500)).min(1).max(40), input.compensatingControls, 'Compensating controls');
489
+ const evidence = parseWith(z.array(evidenceReference).min(1).max(40), input.evidence, 'Policy exception evidence');
490
+ const expiresAt = String(input.expiresAt ?? '');
491
+ const now = options.now ?? new Date().toISOString();
492
+ if (Number.isNaN(Date.parse(expiresAt)) || new Date(expiresAt) <= new Date(now)) throw new Error('Policy exception requires a future expiry');
493
+ const decisionDigest = sha({ evaluationDigest: evaluation.evaluationDigest, ruleId: rule.qualifiedRuleId, reviewerRole, approvedBy, owner, scope, rationale, compensatingControls, evidence, expiresAt });
494
+ const record = {
495
+ schema: 'ewai.policy-exception/v1', decisionDigest,
496
+ evaluationDigest: evaluation.evaluationDigest, intentReference: reference,
497
+ ruleId: rule.qualifiedRuleId, reviewerRole, approvedBy, owner, scope, rationale,
498
+ compensatingControls, evidence, expiresAt, approvedAt: now,
499
+ authority: { namedHumanDecision: true, boundedException: true, buildApproved: false, releaseApproved: false },
500
+ };
501
+ const paths = policyPaths(projectRoot, reference);
502
+ if (decisionRecords(paths.reviewsRoot, 'ewai.policy-review/v1').some((candidate) => (
503
+ candidate.evaluationDigest === evaluation.evaluationDigest && candidate.ruleId === rule.qualifiedRuleId
504
+ ))) throw new Error(`Policy rule ${rule.qualifiedRuleId} already has a current decision; create a new evaluation before replacing it`);
505
+ return persistImmutableDecision(paths.exceptionsRoot, rule.qualifiedRuleId, decisionDigest, record);
506
+ }
507
+
508
+ function decisionRecordDigest(record) {
509
+ if (record.schema === 'ewai.policy-review/v1') {
510
+ return sha({
511
+ evaluationDigest: record.evaluationDigest,
512
+ ruleId: record.ruleId,
513
+ reviewerRole: record.reviewerRole,
514
+ reviewedBy: record.reviewedBy,
515
+ decision: record.decision,
516
+ rationale: record.rationale,
517
+ evidence: record.evidence,
518
+ controls: record.controls,
519
+ });
520
+ }
521
+ if (record.schema === 'ewai.policy-exception/v1') {
522
+ return sha({
523
+ evaluationDigest: record.evaluationDigest,
524
+ ruleId: record.ruleId,
525
+ reviewerRole: record.reviewerRole,
526
+ approvedBy: record.approvedBy,
527
+ owner: record.owner,
528
+ scope: record.scope,
529
+ rationale: record.rationale,
530
+ compensatingControls: record.compensatingControls,
531
+ evidence: record.evidence,
532
+ expiresAt: record.expiresAt,
533
+ });
534
+ }
535
+ return null;
536
+ }
537
+
538
+ function decisionRecords(root, schema) {
539
+ if (!existsSync(root)) return [];
540
+ return readdirSync(root, { withFileTypes: true })
541
+ .filter((entry) => entry.isFile() && entry.name.endsWith('.json'))
542
+ .map((entry) => {
543
+ try {
544
+ return readJson(resolve(root, entry.name), 'Policy decision');
545
+ } catch {
546
+ return null;
547
+ }
548
+ })
549
+ .filter((record) => (
550
+ record?.schema === schema
551
+ && record.authority?.namedHumanDecision === true
552
+ && decisionRecordDigest(record) === record.decisionDigest
553
+ ));
554
+ }
555
+
556
+ export function readPolicyEvaluationStatus(projectRoot, reference, options = {}) {
557
+ const checked = intentReference.parse(reference);
558
+ const baseline = readPolicyBaselineStatus(projectRoot);
559
+ if (baseline.status !== 'enabled') return { schema: 'ewai.policy-evaluation-status/v1', status: baseline.status, blocking: true, baseline };
560
+ const paths = policyPaths(projectRoot, checked);
561
+ if (!existsSync(paths.evaluationPath)) return { schema: 'ewai.policy-evaluation-status/v1', status: 'not-evaluated', blocking: true };
562
+ let evaluation;
563
+ try {
564
+ evaluation = currentEvaluation(projectRoot, checked);
565
+ } catch (error) {
566
+ return { schema: 'ewai.policy-evaluation-status/v1', status: 'invalid', blocking: true, notice: error.message };
567
+ }
568
+ try {
569
+ assertEvaluationCurrent(projectRoot, evaluation);
570
+ } catch (error) {
571
+ return { schema: 'ewai.policy-evaluation-status/v1', status: 'stale', blocking: true, notice: error.message, evaluationDigest: evaluation.evaluationDigest };
572
+ }
573
+ const reviews = decisionRecords(paths.reviewsRoot, 'ewai.policy-review/v1').filter((record) => record.evaluationDigest === evaluation.evaluationDigest);
574
+ const at = new Date(options.now ?? new Date().toISOString());
575
+ const exceptions = decisionRecords(paths.exceptionsRoot, 'ewai.policy-exception/v1').filter((record) => (
576
+ record.evaluationDigest === evaluation.evaluationDigest && new Date(record.expiresAt) > at
577
+ ));
578
+ const duplicateDecision = [...reviews, ...exceptions].find((record, index, records) => (
579
+ records.findIndex((candidate) => candidate.ruleId === record.ruleId) !== index
580
+ ));
581
+ if (duplicateDecision) {
582
+ return {
583
+ schema: 'ewai.policy-evaluation-status/v1',
584
+ status: 'invalid',
585
+ blocking: true,
586
+ notice: `Multiple current decisions exist for ${duplicateDecision.ruleId}; create a new evaluation and reconcile the evidence.`,
587
+ evaluationDigest: evaluation.evaluationDigest,
588
+ };
589
+ }
590
+ const effective = [];
591
+ const decisionControls = [];
592
+ for (const rule of evaluation.matchedRules) {
593
+ const exception = exceptions.find((record) => record.ruleId === rule.qualifiedRuleId);
594
+ if (exception) {
595
+ effective.push('allow-with-controls');
596
+ decisionControls.push(...exception.compensatingControls.map((description) => ({ ruleId: rule.qualifiedRuleId, description, source: 'exception' })));
597
+ continue;
598
+ }
599
+ if (rule.outcome === 'review-required') {
600
+ const review = reviews.find((record) => record.ruleId === rule.qualifiedRuleId);
601
+ if (review) {
602
+ effective.push(review.decision);
603
+ decisionControls.push(...review.controls.map((description) => ({ ruleId: rule.qualifiedRuleId, description, source: 'review' })));
604
+ } else effective.push('review-required');
605
+ continue;
606
+ }
607
+ effective.push(rule.outcome);
608
+ }
609
+ effective.push(...evaluation.unmatchedPolicies.map(({ outcome }) => outcome));
610
+ const status = governingOutcome(effective);
611
+ return {
612
+ schema: 'ewai.policy-evaluation-status/v1',
613
+ status,
614
+ blocking: ['deny', 'review-required', 'unassessed'].includes(status),
615
+ evaluationDigest: evaluation.evaluationDigest,
616
+ policyDigest: evaluation.policyDigest,
617
+ factsDigest: evaluation.factsDigest,
618
+ reviews,
619
+ exceptions,
620
+ controls: [...evaluation.controls, ...decisionControls],
621
+ authority: { designTimeOnly: true, productionEnforcement: false, releaseApproved: false },
622
+ };
623
+ }