@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,535 @@
1
+ import { existsSync, mkdirSync, readFileSync, readdirSync, rmSync } from 'node:fs';
2
+ import { dirname, extname, isAbsolute, posix, relative, resolve } from 'node:path';
3
+ import { atomicJson, atomicText, deliveryPaths, isWithin, sha256 } from '../delivery-documents.mjs';
4
+ import {
5
+ repositoryIndexFreshness,
6
+ repositoryIndexStatus,
7
+ repositorySourceMapCoverage,
8
+ repositoryStandards
9
+ } from './repository-index.mjs';
10
+ import { openRuntimeDatabase } from './database.mjs';
11
+ import { selectContextualPersonas } from './persona-engagement.mjs';
12
+
13
+ const ASSESSMENT_SCHEMA = 'ewai.impact-assessment/v1';
14
+ const PREVIEW_SCHEMA = 'ewai.impact-preview/v1';
15
+ const MAX_SUMMARY = 2_000;
16
+ const MAX_TARGETS = 12;
17
+ const MAX_TARGET = 240;
18
+ const MAX_DEPTH = 2;
19
+ const MAX_NODES = 80;
20
+ const moduleExtensions = ['.mjs', '.js', '.ts', '.vue', '.php'];
21
+ const routeDecisions = new Set(['required', 'recommended', 'not-indicated']);
22
+ const policyDimensionKeys = Object.freeze(['actors', 'aiUse', 'data', 'destinations', 'hosting', 'integrations']);
23
+
24
+ function failure(message, statusCode = 409) {
25
+ const error = new Error(message);
26
+ error.statusCode = statusCode;
27
+ return error;
28
+ }
29
+
30
+ function cleanText(value) {
31
+ return String(value ?? '').trim();
32
+ }
33
+
34
+ function unique(values) {
35
+ return [...new Set(values.filter(Boolean))];
36
+ }
37
+
38
+ function policyDimensions(value) {
39
+ if (value == null) return Object.fromEntries(policyDimensionKeys.map((key) => [key, []]));
40
+ if (!value || typeof value !== 'object' || Array.isArray(value)) throw failure('Policy-material Impact dimensions must be an object', 400);
41
+ const unknown = Object.keys(value).filter((key) => !policyDimensionKeys.includes(key));
42
+ if (unknown.length) throw failure(`Unsupported policy-material Impact dimension(s): ${unknown.join(', ')}`, 400);
43
+ return Object.fromEntries(policyDimensionKeys.map((key) => {
44
+ const supplied = value[key] ?? [];
45
+ if (!Array.isArray(supplied) || supplied.length > 40) throw failure(`Policy-material Impact ${key} must be an array of at most 40 values`, 400);
46
+ const values = unique(supplied.map((item) => cleanText(item)).filter(Boolean)).sort();
47
+ if (values.some((item) => item.length > 160 || /[\u0000-\u001f\u007f]/.test(item))) throw failure(`Policy-material Impact ${key} contains an invalid value`, 400);
48
+ return [key, values];
49
+ }));
50
+ }
51
+
52
+ function policyRelevantDigest(value) {
53
+ return `sha256:${sha256(JSON.stringify(value))}`;
54
+ }
55
+
56
+ function safeTarget(value) {
57
+ const target = cleanText(value).replaceAll('\\', '/');
58
+ if (!target || target.length > MAX_TARGET || target.includes('\0') || isAbsolute(target)) {
59
+ throw failure('Impact targets must be bounded repository-relative paths or symbols', 400);
60
+ }
61
+ if (target.split('/').includes('..') || target.startsWith('./') || target.includes('://')) {
62
+ throw failure('Impact targets must be repository-relative and may not traverse outside the project', 400);
63
+ }
64
+ return target;
65
+ }
66
+
67
+ function validateInput(input = {}) {
68
+ const summary = cleanText(input.summary);
69
+ if (!summary || summary.length > MAX_SUMMARY) throw failure(`Impact summary must contain 1-${MAX_SUMMARY} characters`, 400);
70
+ if (!Array.isArray(input.targets) || !input.targets.length || input.targets.length > MAX_TARGETS) {
71
+ throw failure(`Impact analysis requires 1-${MAX_TARGETS} repository-relative paths or symbols`, 400);
72
+ }
73
+ const dimensions = policyDimensions(input.policyDimensions);
74
+ return { summary, targets: unique(input.targets.map(safeTarget)), policyDimensions: dimensions, policyRelevantDigest: policyRelevantDigest(dimensions) };
75
+ }
76
+
77
+ function currentIndex(projectRoot) {
78
+ const status = repositoryIndexStatus(projectRoot);
79
+ const freshness = repositoryIndexFreshness(projectRoot);
80
+ const sourceMap = repositorySourceMapCoverage(projectRoot);
81
+ if (status.status !== 'completed' || !status.run || freshness.stale) {
82
+ const reason = freshness.reason ?? (freshness.summary ? JSON.stringify(freshness.summary) : status.status);
83
+ throw failure(`Repository map is stale or unavailable (${reason}); refresh it before analysing impact`);
84
+ }
85
+ return {
86
+ runId: Number(status.run.id),
87
+ fresh: true,
88
+ status: status.status,
89
+ indexed: Number(status.counts.files ?? 0),
90
+ parsed: Number(status.counts.parsed ?? 0),
91
+ failed: Number(status.counts.failed ?? 0),
92
+ freshness,
93
+ sourceMap
94
+ };
95
+ }
96
+
97
+ function splitStoredPath(stored) {
98
+ const index = stored.indexOf(':');
99
+ return index < 0 ? { repo: '', relative: stored } : { repo: stored.slice(0, index), relative: stored.slice(index + 1) };
100
+ }
101
+
102
+ function publicPath(stored) {
103
+ return splitStoredPath(stored).relative;
104
+ }
105
+
106
+ function resolveImportedPath(importer, sourceModule, knownPaths) {
107
+ if (!sourceModule.startsWith('.')) return '';
108
+ const { repo, relative: importerRelative } = splitStoredPath(importer);
109
+ const base = posix.normalize(posix.join(posix.dirname(importerRelative), sourceModule));
110
+ const candidates = extname(base)
111
+ ? [base]
112
+ : [...moduleExtensions.map((extension) => `${base}${extension}`), ...moduleExtensions.map((extension) => `${base}/index${extension}`)];
113
+ return candidates.map((candidate) => `${repo}:${candidate}`).find((candidate) => knownPaths.has(candidate)) ?? '';
114
+ }
115
+
116
+ function resolveTargets(files, symbols, targets) {
117
+ const resolvedTargets = [];
118
+ const unresolvedTargets = [];
119
+ const ambiguousTargets = [];
120
+ for (const input of targets) {
121
+ const lower = input.toLowerCase();
122
+ const exactFiles = files.filter((file) => file.path.toLowerCase() === lower || publicPath(file.path).toLowerCase() === lower);
123
+ if (exactFiles.length === 1) {
124
+ resolvedTargets.push({ input, kind: 'file', path: exactFiles[0].path, displayPath: publicPath(exactFiles[0].path), resolution: 'exact-path' });
125
+ continue;
126
+ }
127
+ const exactSymbols = symbols.filter((symbol) => symbol.symbol_name.toLowerCase() === lower);
128
+ if (exactSymbols.length === 1) {
129
+ resolvedTargets.push({ input, kind: 'symbol', symbol: exactSymbols[0].symbol_name, path: exactSymbols[0].path, displayPath: publicPath(exactSymbols[0].path), line: exactSymbols[0].line, resolution: 'exact-symbol' });
130
+ continue;
131
+ }
132
+ const suffixFiles = files.filter((file) => publicPath(file.path).toLowerCase().endsWith(lower));
133
+ const partialSymbols = symbols.filter((symbol) => symbol.symbol_name.toLowerCase().includes(lower));
134
+ const candidates = unique([...suffixFiles.map((file) => file.path), ...partialSymbols.map((symbol) => symbol.path)]);
135
+ if (candidates.length === 1) {
136
+ resolvedTargets.push({ input, kind: suffixFiles.length ? 'file' : 'symbol', path: candidates[0], displayPath: publicPath(candidates[0]), resolution: 'unique-match' });
137
+ } else if (candidates.length > 1 || exactFiles.length > 1 || exactSymbols.length > 1) {
138
+ ambiguousTargets.push({ input, candidates: unique([...exactFiles, ...suffixFiles].map((file) => publicPath(file.path)).concat(exactSymbols.concat(partialSymbols).map((symbol) => `${publicPath(symbol.path)}:${symbol.symbol_name}`))).slice(0, 12) });
139
+ } else unresolvedTargets.push(input);
140
+ }
141
+ return { resolvedTargets, unresolvedTargets, ambiguousTargets };
142
+ }
143
+
144
+ function traverseGraph(files, imports, seeds, options = {}) {
145
+ const maxDepth = Math.max(1, Math.min(MAX_DEPTH, Number(options.maxDepth ?? MAX_DEPTH)));
146
+ const maxNodes = Math.max(1, Math.min(MAX_NODES, Number(options.maxNodes ?? MAX_NODES)));
147
+ const knownPaths = new Set(files.map((file) => file.path));
148
+ const outgoing = new Map();
149
+ const incoming = new Map();
150
+ for (const item of imports) {
151
+ const target = resolveImportedPath(item.path, item.source_module, knownPaths);
152
+ if (!target) continue;
153
+ if (!outgoing.has(item.path)) outgoing.set(item.path, []);
154
+ if (!incoming.has(target)) incoming.set(target, []);
155
+ outgoing.get(item.path).push({ path: target, sourceModule: item.source_module, line: item.line });
156
+ incoming.get(target).push({ path: item.path, sourceModule: item.source_module, line: item.line });
157
+ }
158
+
159
+ const nodes = new Map();
160
+ const queue = [];
161
+ for (const seed of unique(seeds)) {
162
+ nodes.set(seed, { path: publicPath(seed), storedPath: seed, direction: 'seed', distance: 0, relation: 'selected target' });
163
+ queue.push({ path: seed, distance: 0 });
164
+ }
165
+ let truncated = false;
166
+ while (queue.length) {
167
+ const current = queue.shift();
168
+ if (current.distance >= maxDepth) continue;
169
+ const neighbours = [
170
+ ...(outgoing.get(current.path) ?? []).map((edge) => ({ ...edge, direction: 'downstream-dependency', relation: 'imports' })),
171
+ ...(incoming.get(current.path) ?? []).map((edge) => ({ ...edge, direction: 'upstream-consumer', relation: 'imports' }))
172
+ ];
173
+ for (const neighbour of neighbours) {
174
+ if (nodes.has(neighbour.path)) continue;
175
+ if (nodes.size >= maxNodes) {
176
+ truncated = true;
177
+ continue;
178
+ }
179
+ const distance = current.distance + 1;
180
+ nodes.set(neighbour.path, {
181
+ path: publicPath(neighbour.path),
182
+ storedPath: neighbour.path,
183
+ direction: neighbour.direction,
184
+ distance,
185
+ relation: neighbour.relation,
186
+ sourceModule: neighbour.sourceModule,
187
+ line: neighbour.line ?? null
188
+ });
189
+ queue.push({ path: neighbour.path, distance });
190
+ }
191
+ }
192
+ return {
193
+ maxDepth,
194
+ maxNodes,
195
+ truncated,
196
+ nodes: [...nodes.values()].map(({ storedPath, ...node }) => node)
197
+ .sort((left, right) => left.distance - right.distance || left.path.localeCompare(right.path))
198
+ };
199
+ }
200
+
201
+ const impactDefinitions = [
202
+ { id: 'user-workflow', label: 'User workflow', patterns: ['user', 'customer', 'workflow', 'journey', 'form', 'screen', 'experience', 'delegated'], personaSignals: ['user workflow', 'acceptance', 'outcomes'] },
203
+ { id: 'public-interface', label: 'Public interface', patterns: ['public', 'api', 'route', 'endpoint', 'interface', 'contract'], personaSignals: ['public interface', 'acceptance', 'integration'] },
204
+ { id: 'data', label: 'Data', patterns: ['data', 'database', 'record', 'storage', 'schema', 'personal'], personaSignals: ['data', 'privacy', 'information governance'] },
205
+ { id: 'security-privacy', label: 'Security and privacy', patterns: ['security', 'permission', 'access', 'authorisation', 'authorization', 'identity', 'privacy', 'authentication'], personaSignals: ['security', 'identity', 'permission', 'privacy'] },
206
+ { id: 'operations', label: 'Operations', patterns: ['operation', 'runtime', 'recovery', 'failure', 'log', 'monitor', 'support', 'deploy'], personaSignals: ['operations', 'recovery', 'service behaviour'] },
207
+ { id: 'maintainability', label: 'Maintainability', patterns: ['refactor', 'internal', 'helper', 'implementation', 'architecture', 'dependency'], personaSignals: ['maintainability', 'architecture', 'code review'] },
208
+ { id: 'testing', label: 'Testing', patterns: ['test', 'spec', 'quality', 'regression'], personaSignals: ['testing', 'quality', 'regression'] },
209
+ { id: 'documentation', label: 'Documentation and training', patterns: ['document', 'guidance', 'training', 'help', 'instruction', 'communication'], personaSignals: ['documentation', 'training', 'communication'] }
210
+ ];
211
+
212
+ function classifyImpacts(summary, observed) {
213
+ const haystack = `${summary} ${observed.nodes.map((node) => node.path).join(' ')}`.toLowerCase();
214
+ const explicitlyInternal = /\binternal\b/.test(summary.toLowerCase()) && /without changing|no change to|unchanged/.test(summary.toLowerCase());
215
+ return impactDefinitions.flatMap((definition) => {
216
+ if (explicitlyInternal && ['user-workflow', 'public-interface', 'documentation'].includes(definition.id)) return [];
217
+ const signals = definition.patterns.filter((pattern) => haystack.includes(pattern));
218
+ if (!signals.length) return [];
219
+ const evidencePaths = observed.nodes.filter((node) => definition.patterns.some((pattern) => node.path.toLowerCase().includes(pattern))).map((node) => node.path);
220
+ return [{
221
+ id: definition.id,
222
+ label: definition.label,
223
+ authority: 'inferred',
224
+ signals,
225
+ evidencePaths: unique(evidencePaths.length ? evidencePaths : observed.nodes.slice(0, 3).map((node) => node.path)),
226
+ personaSignals: definition.personaSignals
227
+ }];
228
+ });
229
+ }
230
+
231
+ function reviewRoutes(impactAreas, observed) {
232
+ const areas = new Set(impactAreas.map((area) => area.id));
233
+ const productRequired = ['user-workflow', 'public-interface', 'documentation'].some((id) => areas.has(id));
234
+ const securityRequired = areas.has('security-privacy') || areas.has('data');
235
+ const operationsRecommended = areas.has('operations');
236
+ const maintainerRequired = observed.truncated || observed.nodes.length > 20;
237
+ return [
238
+ { id: 'product-owner', label: 'Product Owner', recommendation: productRequired ? 'required' : 'not-indicated', reason: productRequired ? 'User workflow, public interface, documentation, training, acceptance, or material outcomes may change.' : 'No user-facing, acceptance, documentation, training, or outcome consequence was found in the bounded evidence.', authorityBoundary: 'Confirms product consequence and acceptance evidence; does not approve Build.' },
239
+ { id: 'security-identity', label: 'Security or identity owner', recommendation: securityRequired ? 'required' : 'not-indicated', reason: securityRequired ? 'Security, identity, permission, privacy, or data signals are present.' : 'No security, identity, privacy, permission, or data signal was found.', authorityBoundary: 'Reviews the relevant security boundary; this assessment is not certification.' },
240
+ { id: 'service-operator', label: 'Service operator', recommendation: operationsRecommended ? 'recommended' : 'not-indicated', reason: operationsRecommended ? 'Operational, recovery, support, logging, or runtime behaviour may change.' : 'No operational consequence was found in the bounded evidence.', authorityBoundary: 'Reviews operability and recovery; does not approve release.' },
241
+ { id: 'maintainer', label: 'Maintainer', recommendation: maintainerRequired ? 'required' : 'recommended', reason: maintainerRequired ? 'The observed radius is broad or truncated and needs explicit maintainability review.' : 'A maintainer should verify dependency, test, and implementation consequences.', authorityBoundary: 'Reviews technical change quality; does not replace product or assurance review.' },
242
+ { id: 'user-validation', label: 'User validation', recommendation: productRequired ? 'recommended' : 'not-indicated', reason: productRequired ? 'A user-visible consequence should be validated with representative people.' : 'No user-visible consequence was found in the bounded evidence.', authorityBoundary: 'Human user evidence takes priority over persona simulation.' }
243
+ ];
244
+ }
245
+
246
+ function applicableStandards(projectRoot, slug, resolvedTargets, observed) {
247
+ const targets = unique([slug, ...resolvedTargets.map((target) => target.displayPath), ...observed.nodes.map((node) => node.path)]);
248
+ const standards = targets.flatMap((target) => repositoryStandards(projectRoot, target).standards);
249
+ return [...new Map(standards.map((standard) => [`${standard.standardId}:${standard.sourcePath}:${standard.targetPath}`, standard])).values()]
250
+ .map((standard) => ({ ...standard, authority: 'applicable-not-compliance' }));
251
+ }
252
+
253
+ export function previewImpactAssessment(projectRoot, slug, input = {}, options = {}) {
254
+ const validated = validateInput(input);
255
+ deliveryPaths(projectRoot, slug);
256
+ const index = currentIndex(projectRoot);
257
+ const database = openRuntimeDatabase(projectRoot);
258
+ let files;
259
+ let symbols;
260
+ let imports;
261
+ try {
262
+ files = database.prepare('SELECT repo, path, language, parser_status FROM repo_files WHERE run_id = ? ORDER BY path').all(index.runId);
263
+ symbols = database.prepare('SELECT symbol_kind, symbol_name, path, line, signature FROM repo_symbols WHERE run_id = ? ORDER BY path, line').all(index.runId);
264
+ imports = database.prepare('SELECT path, source_module, imported_name, local_name, import_kind, line FROM repo_imports WHERE run_id = ? ORDER BY path, line').all(index.runId);
265
+ } finally {
266
+ database.close();
267
+ }
268
+ const resolution = resolveTargets(files, symbols, validated.targets);
269
+ const observed = traverseGraph(files, imports, resolution.resolvedTargets.map((target) => target.path), options);
270
+ const impactAreas = classifyImpacts(validated.summary, observed);
271
+ const signals = unique(impactAreas.flatMap((area) => area.personaSignals));
272
+ const activePersonas = selectContextualPersonas({
273
+ contextLabel: 'the proposed change and its impact',
274
+ signals,
275
+ context: { summary: validated.summary, targets: validated.targets, impactAreas: impactAreas.map((area) => area.label) },
276
+ personaCatalogue: options.personas ?? [],
277
+ limit: 4
278
+ });
279
+ const routes = reviewRoutes(impactAreas, observed);
280
+ const mapIsPartial = Boolean(
281
+ index.sourceMap.outcomes.inventory_only
282
+ || index.sourceMap.outcomes.skipped_sensitive
283
+ || index.sourceMap.outcomes.skipped_oversized
284
+ || index.sourceMap.outcomes.analysis_failed
285
+ || index.sourceMap.depths.shallow
286
+ || index.sourceMap.partial
287
+ );
288
+ const coverage = {
289
+ complete: !mapIsPartial && !index.failed && !resolution.unresolvedTargets.length && !resolution.ambiguousTargets.length && !observed.truncated,
290
+ indexed: index.indexed,
291
+ parsed: index.parsed,
292
+ failed: index.failed,
293
+ unresolved: resolution.unresolvedTargets.length,
294
+ ambiguous: resolution.ambiguousTargets.length,
295
+ truncated: observed.truncated,
296
+ sourceMap: index.sourceMap,
297
+ warning: mapIsPartial || index.failed || resolution.unresolvedTargets.length || resolution.ambiguousTargets.length || observed.truncated
298
+ ? `Coverage is partial. A narrow observed radius is not proof that nothing else changes.${index.sourceMap.warnings.length ? ` ${index.sourceMap.warnings.join(' ')}` : ''}`
299
+ : ''
300
+ };
301
+ const previewId = sha256(JSON.stringify({ slug, runId: index.runId, ...validated })).slice(0, 16);
302
+ return {
303
+ schema: PREVIEW_SCHEMA,
304
+ previewId,
305
+ slug,
306
+ summary: validated.summary,
307
+ targets: validated.targets,
308
+ policyDimensions: validated.policyDimensions,
309
+ policyRelevantDigest: validated.policyRelevantDigest,
310
+ index,
311
+ coverage,
312
+ ...resolution,
313
+ observed,
314
+ impactAreas,
315
+ standards: applicableStandards(projectRoot, slug, resolution.resolvedTargets, observed),
316
+ activePersonas,
317
+ reviewRoutes: routes,
318
+ guidance: { advisory: true, humanEvidenceTakesPriority: true, approvalsChanged: false }
319
+ };
320
+ }
321
+
322
+ function evidenceRoot(projectRoot, slug) {
323
+ return resolve(deliveryPaths(projectRoot, slug).deliveryRoot, 'impacts');
324
+ }
325
+
326
+ function singleLine(value) {
327
+ return cleanText(value).replace(/\s+/g, ' ').replaceAll('`', "'");
328
+ }
329
+
330
+ function renderAssessment(record) {
331
+ const nodeRows = record.observed.nodes.length
332
+ ? record.observed.nodes.map((node) => `| \`${singleLine(node.path)}\` | ${node.direction} | ${node.distance} | ${node.relation} |`).join('\n')
333
+ : '| — | — | — | No repository node resolved |';
334
+ const impactRows = record.impactAreas.length
335
+ ? record.impactAreas.map((area) => `| ${area.label} | inferred | ${area.signals.join(', ')} | ${area.evidencePaths.map((path) => `\`${singleLine(path)}\``).join(', ')} |`).join('\n')
336
+ : '| No consequence inferred | inferred | — | — |';
337
+ const personaRows = record.activePersonas.length
338
+ ? record.activePersonas.map((persona) => `| ${singleLine(persona.name)} | ${persona.tier} | ${persona.matchedSignals.join(', ')} | ${singleLine(persona.engagementReason)} |`).join('\n')
339
+ : '| No installed persona matched | — | — | — |';
340
+ const routeRows = record.reviewRoutes.map((route) => `| ${route.label} | ${route.recommendation} | ${record.decisions[route.id]} | ${singleLine(record.rationales[route.id] ?? '') || '—'} |`).join('\n');
341
+ return `# Impact Assessment — ${singleLine(record.slug)}
342
+
343
+ **Assessment:** \`${record.assessmentId}\`
344
+ **Repository run:** ${record.index.runId}
345
+ **Assessed by:** ${singleLine(record.assessor)}
346
+ **Assessed at:** ${record.assessedAt}
347
+ **Authority:** Advisory evidence only
348
+
349
+ ## Proposed change
350
+
351
+ ${singleLine(record.summary)}
352
+
353
+ Targets: ${record.targets.map((target) => `\`${singleLine(target)}\``).join(', ')}
354
+
355
+ Policy-material Impact digest: \`${record.policyRelevantDigest}\`. This digest changes only when explicit data, integration, hosting, AI-use, actor, or destination assumptions change.
356
+
357
+ ## Observed repository evidence
358
+
359
+ | Path | Direction | Distance | Relationship |
360
+ | --- | --- | ---: | --- |
361
+ ${nodeRows}
362
+
363
+ Coverage: ${record.coverage.complete ? 'complete within configured bounds' : record.coverage.warning}
364
+
365
+ ## Inferred consequences
366
+
367
+ | Area | Authority | Signals | Evidence paths |
368
+ | --- | --- | --- | --- |
369
+ ${impactRows}
370
+
371
+ ## Active personas
372
+
373
+ | Persona | Tier | Matched concerns | Engagement reason |
374
+ | --- | --- | --- | --- |
375
+ ${personaRows}
376
+
377
+ ## Human review decisions
378
+
379
+ | Route | Recommendation | Decision | Rationale |
380
+ | --- | --- | --- | --- |
381
+ ${routeRows}
382
+
383
+ ## Boundary
384
+
385
+ This assessment does not approve Build, security, compliance, release, or Manual QA. Human evidence and accountable reviewers retain authority.
386
+ `;
387
+ }
388
+
389
+ export function confirmImpactAssessment(projectRoot, slug, input = {}, options = {}) {
390
+ if (input.acknowledged !== true) throw failure('Impact assessment requires acknowledgement of evidence and coverage limits', 400);
391
+ const assessor = cleanText(input.assessor);
392
+ if (!assessor || assessor.length > 160) throw failure('Impact assessment requires a named assessor', 400);
393
+ const preview = previewImpactAssessment(projectRoot, slug, input, options);
394
+ if (Number(input.indexRunId) !== preview.index.runId) throw failure('The repository map changed since preview; analyse impact again');
395
+ if (!input.decisions || typeof input.decisions !== 'object' || Array.isArray(input.decisions)) throw failure('Impact route decisions are required', 400);
396
+ const rationales = input.rationales && typeof input.rationales === 'object' && !Array.isArray(input.rationales) ? input.rationales : {};
397
+ const decisions = {};
398
+ const safeRationales = {};
399
+ for (const route of preview.reviewRoutes) {
400
+ const decision = cleanText(input.decisions[route.id]);
401
+ if (!routeDecisions.has(decision)) throw failure(`Impact route ${route.id} requires a valid decision`, 400);
402
+ const rationale = cleanText(rationales[route.id] ?? rationales.override);
403
+ if (decision !== route.recommendation && !rationale) throw failure(`A rationale is required when changing the ${route.label} recommendation`, 400);
404
+ decisions[route.id] = decision;
405
+ if (rationale) safeRationales[route.id] = rationale.slice(0, 1_000);
406
+ }
407
+ const identity = {
408
+ slug,
409
+ summary: preview.summary,
410
+ targets: preview.targets,
411
+ policyRelevantDigest: preview.policyRelevantDigest,
412
+ indexRunId: preview.index.runId,
413
+ decisions,
414
+ rationales: safeRationales,
415
+ assessor
416
+ };
417
+ const assessmentId = sha256(JSON.stringify(identity)).slice(0, 16);
418
+ const assessedAt = options.now ?? new Date().toISOString();
419
+ const record = {
420
+ schema: ASSESSMENT_SCHEMA,
421
+ assessmentId,
422
+ slug,
423
+ previewId: preview.previewId,
424
+ summary: preview.summary,
425
+ targets: preview.targets,
426
+ policyDimensions: preview.policyDimensions,
427
+ policyRelevantDigest: preview.policyRelevantDigest,
428
+ index: preview.index,
429
+ coverage: preview.coverage,
430
+ resolvedTargets: preview.resolvedTargets,
431
+ unresolvedTargets: preview.unresolvedTargets,
432
+ ambiguousTargets: preview.ambiguousTargets,
433
+ observed: preview.observed,
434
+ impactAreas: preview.impactAreas,
435
+ standards: preview.standards,
436
+ activePersonas: preview.activePersonas,
437
+ reviewRoutes: preview.reviewRoutes,
438
+ decisions,
439
+ rationales: safeRationales,
440
+ assessor,
441
+ acknowledged: true,
442
+ assessedAt,
443
+ authority: { advisory: true, approvalsChanged: false, humanEvidenceTakesPriority: true }
444
+ };
445
+ const root = evidenceRoot(projectRoot, slug);
446
+ const jsonPath = resolve(root, `${assessmentId}.json`);
447
+ const markdownPath = resolve(root, `${assessmentId}.md`);
448
+ if (existsSync(jsonPath) || existsSync(markdownPath)) {
449
+ if (!existsSync(jsonPath) || !existsSync(markdownPath)) throw failure(`Impact evidence ${assessmentId} is incomplete and will not be overwritten`);
450
+ const existing = JSON.parse(readFileSync(jsonPath, 'utf8'));
451
+ const existingIdentity = {
452
+ slug: existing.slug,
453
+ summary: existing.summary,
454
+ targets: existing.targets,
455
+ policyRelevantDigest: existing.policyRelevantDigest,
456
+ indexRunId: existing.index?.runId,
457
+ decisions: existing.decisions,
458
+ rationales: existing.rationales,
459
+ assessor: existing.assessor
460
+ };
461
+ const existingAssessmentId = sha256(JSON.stringify(existingIdentity)).slice(0, 16);
462
+ if (existing.assessmentId !== assessmentId || existingAssessmentId !== assessmentId) {
463
+ throw failure(`Impact evidence ${assessmentId} already exists and will not be overwritten`);
464
+ }
465
+ return {
466
+ schema: 'ewai.impact-confirmation/v1', status: 'recorded', assessmentId,
467
+ evidence: { jsonPath: relative(resolve(projectRoot), jsonPath).replaceAll('\\', '/'), markdownPath: relative(resolve(projectRoot), markdownPath).replaceAll('\\', '/') },
468
+ assessment: existing
469
+ };
470
+ }
471
+ mkdirSync(dirname(jsonPath), { recursive: true });
472
+ let jsonCreated = false;
473
+ try {
474
+ atomicJson(jsonPath, record);
475
+ jsonCreated = true;
476
+ atomicText(markdownPath, renderAssessment(record));
477
+ } catch (error) {
478
+ if (jsonCreated) rmSync(jsonPath, { force: true });
479
+ rmSync(markdownPath, { force: true });
480
+ throw error;
481
+ }
482
+ return {
483
+ schema: 'ewai.impact-confirmation/v1', status: 'recorded', assessmentId,
484
+ evidence: { jsonPath: relative(resolve(projectRoot), jsonPath).replaceAll('\\', '/'), markdownPath: relative(resolve(projectRoot), markdownPath).replaceAll('\\', '/') },
485
+ assessment: record
486
+ };
487
+ }
488
+
489
+ export function readImpactWorkspace(projectRoot, slug) {
490
+ deliveryPaths(projectRoot, slug);
491
+ const status = repositoryIndexStatus(projectRoot);
492
+ const freshness = repositoryIndexFreshness(projectRoot);
493
+ const sourceMap = repositorySourceMapCoverage(projectRoot);
494
+ const root = evidenceRoot(projectRoot, slug);
495
+ const assessments = existsSync(root)
496
+ ? readdirSync(root, { withFileTypes: true }).filter((entry) => entry.isFile() && entry.name.endsWith('.json')).flatMap((entry) => {
497
+ try {
498
+ const record = JSON.parse(readFileSync(resolve(root, entry.name), 'utf8'));
499
+ if (record.schema !== ASSESSMENT_SCHEMA) return [];
500
+ return [{
501
+ assessmentId: record.assessmentId,
502
+ summary: record.summary,
503
+ assessor: record.assessor,
504
+ assessedAt: record.assessedAt,
505
+ policyDimensions: record.policyDimensions,
506
+ policyRelevantDigest: record.policyRelevantDigest,
507
+ coverage: record.coverage,
508
+ decisions: record.decisions,
509
+ activePersonas: record.activePersonas,
510
+ jsonPath: relative(resolve(projectRoot), resolve(root, entry.name)).replaceAll('\\', '/'),
511
+ markdownPath: relative(resolve(projectRoot), resolve(root, `${record.assessmentId}.md`)).replaceAll('\\', '/')
512
+ }];
513
+ } catch {
514
+ return [];
515
+ }
516
+ }).sort((left, right) => String(right.assessedAt).localeCompare(String(left.assessedAt)))
517
+ : [];
518
+ return {
519
+ schema: 'ewai.impact-workspace/v1',
520
+ slug,
521
+ index: {
522
+ status: status.status,
523
+ runId: status.run?.id ?? null,
524
+ fresh: status.status === 'completed' && !freshness.stale,
525
+ indexed: Number(status.counts?.files ?? 0),
526
+ parsed: Number(status.counts?.parsed ?? 0),
527
+ failed: Number(status.counts?.failed ?? 0),
528
+ freshness,
529
+ sourceMap
530
+ },
531
+ assessments,
532
+ limits: { summaryCharacters: MAX_SUMMARY, targets: MAX_TARGETS, targetCharacters: MAX_TARGET, depth: MAX_DEPTH, nodes: MAX_NODES },
533
+ guidance: { advisory: true, approvalsChanged: false, humanEvidenceTakesPriority: true }
534
+ };
535
+ }