@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,703 @@
1
+ import { createHash, randomUUID } from 'node:crypto';
2
+ import {
3
+ existsSync,
4
+ lstatSync,
5
+ mkdirSync,
6
+ readFileSync,
7
+ readdirSync,
8
+ realpathSync,
9
+ rmSync,
10
+ renameSync,
11
+ statSync,
12
+ writeFileSync
13
+ } from 'node:fs';
14
+ import { homedir } from 'node:os';
15
+ import { basename, extname, isAbsolute, relative, resolve, sep } from 'node:path';
16
+ import { projectPaths } from './paths.mjs';
17
+ import {
18
+ listPersonas,
19
+ personalPersonaRoot,
20
+ projectPersonaRoot
21
+ } from './personas.mjs';
22
+ import { selectContextualPersonas } from './runtime/persona-engagement.mjs';
23
+ import { publishLifecycleEventSafely } from './runtime/lifecycle-hooks.mjs';
24
+
25
+ export const MEETING_EVIDENCE_DISCLAIMER = 'Security validation is evidence, not certification or proof that this system is secure. Tools can miss vulnerabilities and produce false positives. A qualified human must review the scope, findings, limitations and residual risk before release.';
26
+
27
+ const SOURCE_SCHEMA = 'ewai.meeting-source-private/v1';
28
+ const REGISTRATION_SCHEMA = 'ewai.meeting-source-registration/v1';
29
+ const EXTRACTION_SCHEMA = 'ewai.meeting-extraction-contract/v1';
30
+ const WORKSPACE_SCHEMA = 'ewai.meeting-evidence-workspace/v1';
31
+ const CANDIDATE_SCHEMA = 'ewai.meeting-candidate-bundle/v1';
32
+ const REVIEW_SCHEMA = 'ewai.meeting-review/v1';
33
+ const EVIDENCE_SCHEMA = 'ewai.meeting-evidence/v1';
34
+ const sourceIdPattern = /^meeting\.[a-z0-9][a-z0-9-]{0,59}\.[a-f0-9]{12}$/;
35
+ const classifications = new Set(['public', 'internal', 'confidential', 'restricted']);
36
+ const cloudPolicies = new Set(['allowed', 'denied', 'unknown']);
37
+ const supportedExtensions = new Set(['.md', '.markdown', '.txt', '.text', '.vtt', '.srt']);
38
+ const allowedCandidateTypes = [
39
+ 'task',
40
+ 'decision',
41
+ 'process',
42
+ 'risk',
43
+ 'system',
44
+ 'policy',
45
+ 'requirement',
46
+ 'assumption',
47
+ 'open-question'
48
+ ];
49
+ const MAX_SOURCE_BYTES = 2 * 1024 * 1024;
50
+ const MAX_SOURCE_LINES = 20_000;
51
+ const candidateIdPattern = /^MEC-[0-9]{3,}$/;
52
+ const digestPattern = /^[a-f0-9]{64}$/;
53
+ const reviewDecisions = new Set(['accepted', 'rejected', 'amended', 'deferred']);
54
+
55
+ function normalized(path) {
56
+ return path.split(sep).join('/');
57
+ }
58
+
59
+ function inside(parent, child) {
60
+ const path = relative(parent, child);
61
+ return path === '' || (!path.startsWith('..') && !isAbsolute(path));
62
+ }
63
+
64
+ function slugify(value) {
65
+ return String(value ?? '')
66
+ .toLowerCase()
67
+ .replace(/[^a-z0-9]+/g, '-')
68
+ .replace(/^-+|-+$/g, '')
69
+ .slice(0, 60) || 'source';
70
+ }
71
+
72
+ function cleanText(value, label, max = 200, required = true) {
73
+ const text = String(value ?? '').trim();
74
+ if (required && !text) throw new Error(`Meeting evidence requires ${label}`);
75
+ if (text.length > max) throw new Error(`Meeting evidence ${label} exceeds ${max} characters`);
76
+ return text;
77
+ }
78
+
79
+ function sourceRoot(paths) {
80
+ return resolve(paths.runtimeRoot, 'meeting-evidence/sources');
81
+ }
82
+
83
+ function sourceRecordPath(paths, sourceId) {
84
+ if (!sourceIdPattern.test(String(sourceId ?? ''))) throw new Error(`Invalid meeting source ID: ${sourceId}`);
85
+ const root = sourceRoot(paths);
86
+ const path = resolve(root, `${sourceId}.json`);
87
+ if (!inside(root, path)) throw new Error('Unsafe meeting source path');
88
+ return path;
89
+ }
90
+
91
+ function atomicJson(path, value) {
92
+ mkdirSync(resolve(path, '..'), { recursive: true });
93
+ const temporary = `${path}.${process.pid}.${Date.now()}.tmp`;
94
+ writeFileSync(temporary, `${JSON.stringify(value, null, 2)}\n`, { encoding: 'utf8', mode: 0o600 });
95
+ renameSync(temporary, path);
96
+ }
97
+
98
+ function sha256(value) {
99
+ return createHash('sha256').update(value).digest('hex');
100
+ }
101
+
102
+ function canonicalDigest(value) {
103
+ return sha256(JSON.stringify(value));
104
+ }
105
+
106
+ function safeRelative(projectRoot, path) {
107
+ const value = normalized(relative(projectRoot, path));
108
+ if (!value || value.startsWith('../') || isAbsolute(value)) throw new Error('Meeting evidence path escapes the project');
109
+ return value;
110
+ }
111
+
112
+ function contentFacts(path, stats = statSync(path)) {
113
+ if (stats.size > MAX_SOURCE_BYTES) throw new Error('Meeting source exceeds the 2 MiB limit');
114
+ const content = readFileSync(path);
115
+ const text = content.toString('utf8');
116
+ const lineCount = text ? text.split(/\r?\n/).length - (/\r?\n$/.test(text) ? 1 : 0) : 0;
117
+ if (lineCount > MAX_SOURCE_LINES) throw new Error('Meeting source exceeds the 20,000 lines limit');
118
+ return {
119
+ size: stats.size,
120
+ lineCount,
121
+ digest: createHash('sha256').update(content).digest('hex'),
122
+ modifiedAt: stats.mtime.toISOString()
123
+ };
124
+ }
125
+
126
+ function readPrivateSource(paths, sourceId) {
127
+ const path = sourceRecordPath(paths, sourceId);
128
+ if (!existsSync(path)) throw new Error(`Unknown meeting source: ${sourceId}`);
129
+ const record = JSON.parse(readFileSync(path, 'utf8'));
130
+ if (record.schema !== SOURCE_SCHEMA || record.sourceId !== sourceId) {
131
+ throw new Error(`Invalid meeting source record: ${sourceId}`);
132
+ }
133
+ return record;
134
+ }
135
+
136
+ function reviewRoot(paths) {
137
+ return resolve(paths.runtimeRoot, 'meeting-evidence/reviews');
138
+ }
139
+
140
+ function reviewRecordPath(paths, sourceId) {
141
+ const root = reviewRoot(paths);
142
+ const path = resolve(root, `${sourceId}.json`);
143
+ if (!sourceIdPattern.test(String(sourceId ?? '')) || !inside(root, path)) throw new Error(`Invalid meeting source ID: ${sourceId}`);
144
+ return path;
145
+ }
146
+
147
+ function readReview(paths, sourceId) {
148
+ const path = reviewRecordPath(paths, sourceId);
149
+ if (!existsSync(path)) throw new Error(`Meeting source has no current named review: ${sourceId}`);
150
+ const review = JSON.parse(readFileSync(path, 'utf8'));
151
+ if (review.schema !== REVIEW_SCHEMA || review.sourceId !== sourceId) throw new Error(`Invalid meeting review record: ${sourceId}`);
152
+ const { reviewDigest, ...payload } = review;
153
+ if (!digestPattern.test(String(reviewDigest ?? '')) || canonicalDigest(payload) !== reviewDigest) {
154
+ throw new Error(`Meeting review digest is invalid: ${sourceId}`);
155
+ }
156
+ return review;
157
+ }
158
+
159
+ function evidencePaths(paths, sourceId) {
160
+ const root = resolve(paths.specsRoot, '3.Evidence/meeting-evidence', sourceId);
161
+ if (!inside(paths.specsRoot, root)) throw new Error('Unsafe meeting evidence destination');
162
+ return {
163
+ root,
164
+ json: resolve(root, 'evidence.json'),
165
+ markdown: resolve(root, 'evidence.md')
166
+ };
167
+ }
168
+
169
+ function transactionPaths(paths, sourceId) {
170
+ const root = resolve(paths.runtimeRoot, 'meeting-evidence/transactions');
171
+ return { root, journal: resolve(root, `${sourceId}.json`) };
172
+ }
173
+
174
+ function cleanPromotionTransaction(paths, sourceId) {
175
+ const transaction = transactionPaths(paths, sourceId);
176
+ if (!existsSync(transaction.journal)) return;
177
+ const journal = JSON.parse(readFileSync(transaction.journal, 'utf8'));
178
+ const targets = evidencePaths(paths, sourceId);
179
+ for (const staged of [journal.jsonStage, journal.markdownStage]) {
180
+ const path = resolve(String(staged ?? ''));
181
+ if (inside(targets.root, path) && path.endsWith('.tmp')) rmSync(path, { force: true });
182
+ }
183
+ if (journal.phase !== 'committed') {
184
+ rmSync(targets.json, { force: true });
185
+ rmSync(targets.markdown, { force: true });
186
+ }
187
+ rmSync(transaction.journal, { force: true });
188
+ }
189
+
190
+ function safeSource(record, facts) {
191
+ const current = facts ?? sourceCurrentFacts(record);
192
+ const freshness = current.missing
193
+ ? 'missing'
194
+ : current.digest === record.digest && current.size === record.size
195
+ ? 'current'
196
+ : 'stale';
197
+ return {
198
+ sourceId: record.sourceId,
199
+ label: record.label,
200
+ extension: record.extension,
201
+ classification: record.classification,
202
+ cloudProcessing: record.cloudProcessing,
203
+ size: record.size,
204
+ lineCount: record.lineCount,
205
+ digest: record.digest,
206
+ registeredAt: record.registeredAt,
207
+ modifiedAt: record.modifiedAt,
208
+ freshness
209
+ };
210
+ }
211
+
212
+ function sourceCurrentFacts(record) {
213
+ if (!existsSync(record.sourcePath)) return { missing: true };
214
+ try {
215
+ const stats = statSync(record.sourcePath);
216
+ if (!stats.isFile()) return { missing: true };
217
+ return contentFacts(record.sourcePath, stats);
218
+ } catch {
219
+ return { missing: true };
220
+ }
221
+ }
222
+
223
+ function inferTier(persona) {
224
+ const path = normalized(String(persona.path ?? ''));
225
+ if (persona.tier) return persona.tier;
226
+ if (path.includes('/premium-personas/')) return 'premium';
227
+ if (path.includes('/1.Scope/personas/project/')) return 'project';
228
+ if (path.includes('/.ewai/personas/')) return 'personal';
229
+ return 'core';
230
+ }
231
+
232
+ function installedPersonaCatalogue(projectRoot) {
233
+ const roots = [
234
+ resolve(import.meta.dirname, '../packs/personas/core/personas'),
235
+ resolve(homedir(), '.ewai/packs/ewai.personas.professional/premium-personas'),
236
+ personalPersonaRoot(),
237
+ projectPersonaRoot(projectRoot)
238
+ ];
239
+ const ranks = { project: 4, premium: 3, personal: 2, core: 1 };
240
+ const found = new Map();
241
+ for (const persona of listPersonas(roots)) {
242
+ const candidate = { ...persona, tier: inferTier(persona) };
243
+ const existing = found.get(candidate.id);
244
+ if (!existing || ranks[candidate.tier] > ranks[existing.tier]) found.set(candidate.id, candidate);
245
+ }
246
+ return [...found.values()];
247
+ }
248
+
249
+ function contextualPersonas(projectRoot, source, options = {}) {
250
+ const focus = Array.isArray(options.focus)
251
+ ? options.focus.map((item) => String(item).trim()).filter(Boolean)
252
+ : String(options.focus ?? '').split(',').map((item) => item.trim()).filter(Boolean);
253
+ const signals = focus.length
254
+ ? focus
255
+ : ['meeting', 'evidence', 'provenance', 'review', source.classification, source.cloudProcessing];
256
+ const catalogue = Array.isArray(options.personaCatalogue)
257
+ ? options.personaCatalogue
258
+ : installedPersonaCatalogue(projectRoot);
259
+ return selectContextualPersonas({
260
+ signals,
261
+ context: {
262
+ source: {
263
+ classification: source.classification,
264
+ cloudProcessing: source.cloudProcessing,
265
+ freshness: source.freshness
266
+ },
267
+ focus
268
+ },
269
+ personaCatalogue: catalogue.map((persona) => ({ ...persona, tier: inferTier(persona) })),
270
+ contextLabel: 'meeting evidence extraction',
271
+ limit: options.personaLimit ?? 4
272
+ });
273
+ }
274
+
275
+ function candidateContract(source) {
276
+ return {
277
+ schema: CANDIDATE_SCHEMA,
278
+ schemaPath: 'config/meeting-evidence-candidate.schema.json',
279
+ sourceId: source.sourceId,
280
+ sourceDigest: source.digest,
281
+ sourceLineCount: source.lineCount,
282
+ allowedTypes: [...allowedCandidateTypes],
283
+ confidenceLabels: ['low', 'medium', 'high'],
284
+ requiredBoundaries: ['observedStatement', 'interpretation', 'lineAnchors', 'confidence'],
285
+ rawSourceExcerptsAllowed: false,
286
+ rawModelOutputAllowed: false
287
+ };
288
+ }
289
+
290
+ function normaliseCandidate(raw, source) {
291
+ if (!raw || typeof raw !== 'object' || Array.isArray(raw)) throw new Error('Each meeting candidate must be an object');
292
+ const allowed = new Set(['id', 'type', 'observedStatement', 'interpretation', 'lineAnchors', 'confidence']);
293
+ for (const key of Object.keys(raw)) if (!allowed.has(key)) throw new Error(`Meeting candidate field is not allowed: ${key}`);
294
+ const id = cleanText(raw.id, 'candidate ID', 30);
295
+ if (!candidateIdPattern.test(id)) throw new Error(`Invalid meeting candidate ID: ${id}`);
296
+ const type = cleanText(raw.type, 'candidate type', 40);
297
+ if (!allowedCandidateTypes.includes(type)) throw new Error(`Unsupported meeting candidate type: ${type}`);
298
+ const observedStatement = cleanText(raw.observedStatement, 'candidate observed statement', 600);
299
+ const interpretation = cleanText(raw.interpretation, 'candidate interpretation', 1000);
300
+ const confidence = cleanText(raw.confidence, 'candidate confidence', 20);
301
+ if (!['low', 'medium', 'high'].includes(confidence)) throw new Error(`Unsupported meeting candidate confidence: ${confidence}`);
302
+ if (!Array.isArray(raw.lineAnchors) || !raw.lineAnchors.length || raw.lineAnchors.length > 20) {
303
+ throw new Error(`${id} requires one to 20 line anchors`);
304
+ }
305
+ const lineAnchors = raw.lineAnchors.map((anchor) => {
306
+ const start = Number(anchor?.start);
307
+ const end = Number(anchor?.end);
308
+ if (!Number.isInteger(start) || !Number.isInteger(end) || start < 1 || end < start || end > source.lineCount) {
309
+ throw new Error(`${id} has an invalid line anchor`);
310
+ }
311
+ return { start, end };
312
+ });
313
+ return { id, type, observedStatement, interpretation, lineAnchors, confidence };
314
+ }
315
+
316
+ function normaliseBundle(raw, source) {
317
+ if (!raw || typeof raw !== 'object' || Array.isArray(raw)) throw new Error('Meeting candidate bundle must be an object');
318
+ const allowed = new Set(['schema', 'sourceId', 'sourceDigest', 'candidates']);
319
+ for (const key of Object.keys(raw)) if (!allowed.has(key)) throw new Error(`Meeting candidate bundle field is not allowed: ${key}`);
320
+ if (raw.schema !== CANDIDATE_SCHEMA) throw new Error(`Meeting candidate bundle schema must be ${CANDIDATE_SCHEMA}`);
321
+ if (raw.sourceId !== source.sourceId) throw new Error('Meeting candidate bundle source does not match');
322
+ if (raw.sourceDigest !== source.digest) throw new Error('Meeting candidate bundle source digest is stale');
323
+ if (!Array.isArray(raw.candidates) || !raw.candidates.length || raw.candidates.length > 500) {
324
+ throw new Error('Meeting candidate bundle must contain one to 500 candidates');
325
+ }
326
+ const candidates = raw.candidates.map((candidate) => normaliseCandidate(candidate, source));
327
+ if (new Set(candidates.map(({ id }) => id)).size !== candidates.length) throw new Error('Meeting candidate bundle contains a duplicate candidate ID');
328
+ return { schema: CANDIDATE_SCHEMA, sourceId: source.sourceId, sourceDigest: source.digest, candidates };
329
+ }
330
+
331
+ function normaliseDispositions(raw, candidates) {
332
+ if (!Array.isArray(raw) || raw.length !== candidates.length) throw new Error('Meeting review must dispose of every candidate exactly once');
333
+ const known = new Set(candidates.map(({ id }) => id));
334
+ const dispositions = raw.map((item) => {
335
+ const candidateId = cleanText(item?.candidateId, 'review candidate ID', 30);
336
+ if (!known.has(candidateId)) throw new Error(`Meeting review references an unknown candidate: ${candidateId}`);
337
+ const decision = cleanText(item?.decision, 'review decision', 20);
338
+ if (!reviewDecisions.has(decision)) throw new Error(`Unsupported meeting review decision: ${decision}`);
339
+ const replacementText = cleanText(item?.replacementText, 'amendment replacement text', 1000, false);
340
+ const rationale = cleanText(item?.rationale, 'review rationale', 1000, false);
341
+ if (decision === 'amended' && (!replacementText || !rationale)) throw new Error(`${candidateId} amendment requires replacement text and rationale`);
342
+ if (decision !== 'amended' && replacementText) throw new Error(`${candidateId} replacement text is allowed only for an amendment`);
343
+ return { candidateId, decision, ...(replacementText ? { replacementText } : {}), ...(rationale ? { rationale } : {}) };
344
+ });
345
+ if (new Set(dispositions.map(({ candidateId }) => candidateId)).size !== dispositions.length) {
346
+ throw new Error('Meeting review must dispose of every candidate exactly once');
347
+ }
348
+ return dispositions;
349
+ }
350
+
351
+ function renderEvidence(record) {
352
+ const items = record.evidence.map((item) => `### ${item.id} — ${item.type}\n\n- **Statement:** ${item.statement}\n- **Interpretation:** ${item.interpretation}\n- **Confidence:** ${item.confidence}\n- **Source lines:** ${item.lineAnchors.map(({ start, end }) => start === end ? `${start}` : `${start}-${end}`).join(', ')}\n- **Review decision:** ${item.reviewDecision}\n`).join('\n');
353
+ return `# Meeting Evidence — ${record.source.label}\n\n**Schema:** \`${record.schema}\` \n**Source ID:** \`${record.source.sourceId}\` \n**Source digest:** \`${record.source.digest}\` \n**Reviewed by:** ${record.review.reviewedBy} \n**Approved by:** ${record.promotion.approvedBy} \n**Approved at:** ${record.promotion.approvedAt} \n**Evidence digest:** \`${record.evidenceDigest}\`\n\n## Reviewed evidence\n\n${items}\n## Authority boundary\n\nThis pack records reviewed meeting evidence only. It does not create an intent, task, requirement, policy, architecture decision, delivery approval, accepted risk or release authority.\n\n${MEETING_EVIDENCE_DISCLAIMER}\n`;
354
+ }
355
+
356
+ function assertInitialized(paths) {
357
+ if (!existsSync(paths.configPath)) throw new Error('Initialize EWAI before registering meeting evidence');
358
+ }
359
+
360
+ export function registerMeetingSource(projectRoot, file, options = {}) {
361
+ if (!options.confirmed) throw new Error('Meeting source registration requires explicit user confirmation');
362
+ const paths = projectPaths(projectRoot);
363
+ assertInitialized(paths);
364
+
365
+ const rawPath = cleanText(file, 'source file', 4096);
366
+ if (rawPath.split(/[\\/]/).includes('..')) throw new Error('Meeting source path traversal is not allowed');
367
+ const candidate = resolve(rawPath);
368
+ if (!existsSync(candidate)) throw new Error('Meeting source is not a readable regular file');
369
+ const linkStats = lstatSync(candidate);
370
+ if (linkStats.isSymbolicLink()) throw new Error('Meeting source must not be a symbolic link');
371
+ if (!linkStats.isFile()) throw new Error('Meeting source must be a readable regular file');
372
+ const sourcePath = realpathSync(candidate);
373
+ const extension = extname(sourcePath).toLowerCase();
374
+ if (!supportedExtensions.has(extension)) throw new Error(`Unsupported meeting source type: ${extension || '(none)'}`);
375
+
376
+ const classification = options.classification ?? 'confidential';
377
+ if (!classifications.has(classification)) throw new Error(`Unsupported meeting source classification: ${classification}`);
378
+ const cloudProcessing = options.cloudProcessing ?? 'unknown';
379
+ if (!cloudPolicies.has(cloudProcessing)) throw new Error(`Unsupported meeting source cloud-processing policy: ${cloudProcessing}`);
380
+
381
+ const facts = contentFacts(sourcePath, statSync(sourcePath));
382
+ const existingSource = existsSync(sourceRoot(paths))
383
+ ? readdirSync(sourceRoot(paths), { withFileTypes: true })
384
+ .filter((entry) => entry.isFile() && entry.name.endsWith('.json'))
385
+ .map((entry) => {
386
+ try {
387
+ return JSON.parse(readFileSync(resolve(sourceRoot(paths), entry.name), 'utf8'));
388
+ } catch {
389
+ return null;
390
+ }
391
+ })
392
+ .find((record) => record?.schema === SOURCE_SCHEMA && record.sourcePath === sourcePath)
393
+ : null;
394
+ if (existingSource) throw new Error(`Meeting source is already registered: ${existingSource.sourceId}`);
395
+ const project = realpathSync(paths.projectRoot);
396
+ const identity = createHash('sha256').update(`${project}\0${sourcePath}`).digest('hex').slice(0, 12);
397
+ const requestedLabel = String(options.label ?? '').trim();
398
+ const label = requestedLabel
399
+ ? cleanText(requestedLabel, 'source label', 160)
400
+ : `Meeting source ${identity.slice(0, 6)}`;
401
+ const sourceId = `meeting.${slugify(label)}.${identity}`;
402
+ const path = sourceRecordPath(paths, sourceId);
403
+ if (existsSync(path)) throw new Error(`Meeting source is already registered: ${sourceId}`);
404
+
405
+ const registeredAt = new Date(options.now ?? Date.now()).toISOString();
406
+ const record = {
407
+ schema: SOURCE_SCHEMA,
408
+ sourceId,
409
+ label,
410
+ sourcePath,
411
+ extension,
412
+ classification,
413
+ cloudProcessing,
414
+ ...facts,
415
+ registeredAt
416
+ };
417
+ atomicJson(path, record);
418
+ return {
419
+ schema: REGISTRATION_SCHEMA,
420
+ source: safeSource(record, facts),
421
+ notices: [
422
+ 'Raw meeting material remains in its original file and is not copied into SPECS or the runtime database.',
423
+ 'Personas provide advisory lenses; they do not approve evidence or replace accountable human review.',
424
+ MEETING_EVIDENCE_DISCLAIMER
425
+ ]
426
+ };
427
+ }
428
+
429
+ export function prepareMeetingExtraction(projectRoot, sourceId, options = {}) {
430
+ const paths = projectPaths(projectRoot);
431
+ assertInitialized(paths);
432
+ const record = readPrivateSource(paths, sourceId);
433
+ const facts = sourceCurrentFacts(record);
434
+ const source = safeSource(record, facts);
435
+ if (source.freshness !== 'current') throw new Error(`Meeting source is ${source.freshness}; register or review the current source before extraction`);
436
+ const permitted = source.cloudProcessing === 'allowed';
437
+ const activePersonas = contextualPersonas(paths.projectRoot, source, options);
438
+ return {
439
+ schema: EXTRACTION_SCHEMA,
440
+ source,
441
+ processing: {
442
+ permitted,
443
+ route: permitted ? 'host-model-with-skill' : 'manual-local-handoff',
444
+ reason: permitted
445
+ ? 'Cloud processing is explicitly allowed for this registered source.'
446
+ : `Cloud processing is ${source.cloudProcessing}; host-model source inspection is not permitted.`
447
+ },
448
+ candidateContract: candidateContract(source),
449
+ activePersonas,
450
+ authority: {
451
+ personasAreAdvisory: true,
452
+ namedReviewRequired: true,
453
+ promotionRequiresSeparateApproval: true,
454
+ downstreamCanonicalAuthoringAllowed: false
455
+ },
456
+ notices: [
457
+ 'Do not copy raw source content, source excerpts, prompts or model output into the candidate record.',
458
+ 'The standard host model and project/core personas are complete; installed premium and personal personas are optional enrichment only.',
459
+ MEETING_EVIDENCE_DISCLAIMER
460
+ ]
461
+ };
462
+ }
463
+
464
+ export function recordMeetingReview(projectRoot, sourceId, input = {}) {
465
+ const paths = projectPaths(projectRoot);
466
+ assertInitialized(paths);
467
+ const record = readPrivateSource(paths, sourceId);
468
+ const source = safeSource(record);
469
+ if (source.freshness !== 'current') throw new Error(`Meeting source is ${source.freshness}; review requires the current source`);
470
+ const reviewedBy = cleanText(input.reviewedBy, 'named reviewer', 160);
471
+ const bundle = normaliseBundle(input.bundle, source);
472
+ const dispositions = normaliseDispositions(input.dispositions, bundle.candidates);
473
+ const reviewedAt = new Date(input.now ?? Date.now()).toISOString();
474
+ const payload = {
475
+ schema: REVIEW_SCHEMA,
476
+ sourceId,
477
+ sourceDigest: source.digest,
478
+ bundleDigest: canonicalDigest(bundle),
479
+ reviewedBy,
480
+ reviewedAt,
481
+ candidates: bundle.candidates,
482
+ dispositions
483
+ };
484
+ const reviewDigest = canonicalDigest(payload);
485
+ const review = { ...payload, reviewDigest };
486
+ const path = reviewRecordPath(paths, sourceId);
487
+ if (existsSync(path)) {
488
+ const existing = readReview(paths, sourceId);
489
+ if (existing.reviewDigest === reviewDigest) {
490
+ return { schema: 'ewai.meeting-review-recording/v1', sourceId, reviewDigest, reviewedBy, reviewedAt: existing.reviewedAt, idempotent: true };
491
+ }
492
+ throw new Error('Conflicting named meeting review already exists and will not be overwritten');
493
+ }
494
+ atomicJson(path, review);
495
+ const counts = Object.fromEntries([...reviewDecisions].map((decision) => [decision, dispositions.filter((item) => item.decision === decision).length]));
496
+ return { schema: 'ewai.meeting-review-recording/v1', sourceId, reviewDigest, reviewedBy, reviewedAt, counts, idempotent: false };
497
+ }
498
+
499
+ export function promoteMeetingEvidence(projectRoot, sourceId, input = {}) {
500
+ if (input.confirmed !== true) throw new Error('Meeting evidence promotion requires exact confirmation');
501
+ const approvedBy = cleanText(input.approvedBy, 'named approver', 160);
502
+ const paths = projectPaths(projectRoot);
503
+ assertInitialized(paths);
504
+ cleanPromotionTransaction(paths, sourceId);
505
+ const privateSource = readPrivateSource(paths, sourceId);
506
+ const source = safeSource(privateSource);
507
+ if (source.freshness !== 'current') throw new Error(`Meeting source is ${source.freshness}; promotion requires the reviewed source digest`);
508
+ const review = readReview(paths, sourceId);
509
+ if (review.sourceDigest !== source.digest) throw new Error('Meeting source is stale relative to the named review');
510
+ const dispositions = new Map(review.dispositions.map((item) => [item.candidateId, item]));
511
+ const evidence = review.candidates.flatMap((candidate) => {
512
+ const decision = dispositions.get(candidate.id);
513
+ if (!['accepted', 'amended'].includes(decision.decision)) return [];
514
+ return [{
515
+ id: candidate.id,
516
+ type: candidate.type,
517
+ statement: decision.decision === 'amended' ? decision.replacementText : candidate.observedStatement,
518
+ interpretation: candidate.interpretation,
519
+ lineAnchors: candidate.lineAnchors,
520
+ confidence: candidate.confidence,
521
+ reviewDecision: decision.decision,
522
+ ...(decision.rationale ? { rationale: decision.rationale } : {})
523
+ }];
524
+ });
525
+ if (!evidence.length) throw new Error('Meeting review has no accepted or amended evidence to promote');
526
+ const targets = evidencePaths(paths, sourceId);
527
+ if (existsSync(targets.json) || existsSync(targets.markdown)) {
528
+ if (!existsSync(targets.json) || !existsSync(targets.markdown)) throw new Error('Existing meeting evidence pair is incomplete');
529
+ const existing = JSON.parse(readFileSync(targets.json, 'utf8'));
530
+ const samePromotion = existing.source?.digest === source.digest
531
+ && existing.review?.digest === review.reviewDigest
532
+ && existing.promotion?.approvedBy === approvedBy
533
+ && JSON.stringify(existing.evidence) === JSON.stringify(evidence)
534
+ && sha256(readFileSync(targets.markdown, 'utf8')) === existing.markdownDigest;
535
+ if (samePromotion) {
536
+ return {
537
+ schema: 'ewai.meeting-promotion-result/v1', sourceId, evidenceDigest: existing.evidenceDigest,
538
+ jsonPath: safeRelative(paths.projectRoot, targets.json), markdownPath: safeRelative(paths.projectRoot, targets.markdown),
539
+ counts: { promoted: evidence.length, rejected: review.dispositions.filter(({ decision }) => decision === 'rejected').length, deferred: review.dispositions.filter(({ decision }) => decision === 'deferred').length },
540
+ lifecycle: { status: 'already-published' }, idempotent: true
541
+ };
542
+ }
543
+ throw new Error('Conflicting meeting evidence already exists and will not be overwritten');
544
+ }
545
+ const approvedAt = new Date(input.now ?? Date.now()).toISOString();
546
+ const payload = {
547
+ schema: EVIDENCE_SCHEMA,
548
+ source: {
549
+ sourceId: source.sourceId,
550
+ label: source.label,
551
+ classification: source.classification,
552
+ digest: source.digest,
553
+ lineCount: source.lineCount
554
+ },
555
+ review: { digest: review.reviewDigest, reviewedBy: review.reviewedBy, reviewedAt: review.reviewedAt },
556
+ promotion: { approvedBy, approvedAt },
557
+ evidence
558
+ };
559
+ const evidenceDigest = canonicalDigest(payload);
560
+ const record = { ...payload, evidenceDigest };
561
+ const markdown = renderEvidence(record);
562
+ record.markdownDigest = sha256(markdown);
563
+ const json = `${JSON.stringify(record, null, 2)}\n`;
564
+ const transaction = transactionPaths(paths, sourceId);
565
+ mkdirSync(targets.root, { recursive: true });
566
+ mkdirSync(transaction.root, { recursive: true });
567
+ const nonce = `${process.pid}-${randomUUID()}`;
568
+ const jsonStage = `${targets.json}.${nonce}.tmp`;
569
+ const markdownStage = `${targets.markdown}.${nonce}.tmp`;
570
+ const journal = { schema: 'ewai.meeting-evidence-transaction/v1', sourceId, phase: 'staged', jsonStage, markdownStage };
571
+ try {
572
+ writeFileSync(jsonStage, json, 'utf8');
573
+ writeFileSync(markdownStage, markdown, 'utf8');
574
+ atomicJson(transaction.journal, journal);
575
+ renameSync(jsonStage, targets.json);
576
+ journal.phase = 'json-committed';
577
+ atomicJson(transaction.journal, journal);
578
+ if (input.testHooks?.failAfterStage === 'json-committed') throw new Error('Meeting evidence write was interrupted after JSON commit');
579
+ renameSync(markdownStage, targets.markdown);
580
+ journal.phase = 'committed';
581
+ atomicJson(transaction.journal, journal);
582
+ rmSync(transaction.journal, { force: true });
583
+ } catch (error) {
584
+ rmSync(jsonStage, { force: true });
585
+ rmSync(markdownStage, { force: true });
586
+ rmSync(targets.json, { force: true });
587
+ rmSync(targets.markdown, { force: true });
588
+ rmSync(transaction.journal, { force: true });
589
+ throw error;
590
+ }
591
+
592
+ const jsonPath = safeRelative(paths.projectRoot, targets.json);
593
+ const markdownPath = safeRelative(paths.projectRoot, targets.markdown);
594
+ const publication = publishLifecycleEventSafely(paths.projectRoot, 'ewai.meeting-evidence.promoted', {
595
+ sourceKey: `meeting-evidence:${sourceId}:${evidenceDigest}`,
596
+ sourceRevision: evidenceDigest,
597
+ occurredAt: approvedAt,
598
+ streamId: `meeting-evidence:${sourceId}`,
599
+ facts: { sourceId, evidenceDigest, candidateCount: evidence.length, approvedAt },
600
+ evidence: [jsonPath, markdownPath]
601
+ });
602
+ return {
603
+ schema: 'ewai.meeting-promotion-result/v1', sourceId, evidenceDigest, jsonPath, markdownPath,
604
+ counts: { promoted: evidence.length, rejected: review.dispositions.filter(({ decision }) => decision === 'rejected').length, deferred: review.dispositions.filter(({ decision }) => decision === 'deferred').length },
605
+ lifecycle: { ...publication, status: publication.event ? 'published' : publication.status ?? 'publication-failed' },
606
+ idempotent: false
607
+ };
608
+ }
609
+
610
+ export function readMeetingEvidenceWorkspace(projectRoot, options = {}) {
611
+ const paths = projectPaths(projectRoot);
612
+ assertInitialized(paths);
613
+ const root = sourceRoot(paths);
614
+ const sources = existsSync(root)
615
+ ? readdirSync(root, { withFileTypes: true })
616
+ .filter((entry) => entry.isFile() && entry.name.endsWith('.json'))
617
+ .sort((left, right) => left.name.localeCompare(right.name))
618
+ .map((entry) => {
619
+ const record = JSON.parse(readFileSync(resolve(root, entry.name), 'utf8'));
620
+ if (record.schema !== SOURCE_SCHEMA) return null;
621
+ const source = safeSource(record);
622
+ const reviewPath = reviewRecordPath(paths, source.sourceId);
623
+ const targets = evidencePaths(paths, source.sourceId);
624
+ const privateReview = existsSync(reviewPath) ? readReview(paths, source.sourceId) : null;
625
+ const promoted = existsSync(targets.json) && existsSync(targets.markdown)
626
+ ? JSON.parse(readFileSync(targets.json, 'utf8'))
627
+ : null;
628
+ return {
629
+ ...source,
630
+ reviewState: privateReview ? 'reviewed' : 'pending',
631
+ promotionState: promoted ? 'promoted' : 'not-promoted',
632
+ candidateCount: privateReview?.candidates?.length ?? 0,
633
+ reviewedCount: privateReview?.dispositions?.length ?? 0,
634
+ promotedCount: promoted?.evidence?.length ?? 0
635
+ };
636
+ })
637
+ .filter(Boolean)
638
+ : [];
639
+ const selected = options.sourceId
640
+ ? sources.find((source) => source.sourceId === options.sourceId) ?? null
641
+ : sources[0] ?? null;
642
+ if (options.sourceId && !selected) throw new Error(`Unknown meeting source: ${options.sourceId}`);
643
+ const activePersonas = selected ? contextualPersonas(paths.projectRoot, selected, options) : [];
644
+ let review = null;
645
+ let candidates = [];
646
+ let promotion = null;
647
+ if (selected?.reviewState === 'reviewed') {
648
+ const privateReview = readReview(paths, selected.sourceId);
649
+ const dispositions = new Map(privateReview.dispositions.map((item) => [item.candidateId, item]));
650
+ review = {
651
+ digest: privateReview.reviewDigest,
652
+ reviewedBy: privateReview.reviewedBy,
653
+ reviewedAt: privateReview.reviewedAt,
654
+ sourceDigest: privateReview.sourceDigest,
655
+ fresh: privateReview.sourceDigest === selected.digest && selected.freshness === 'current'
656
+ };
657
+ candidates = privateReview.candidates.map((candidate) => ({
658
+ ...candidate,
659
+ disposition: dispositions.get(candidate.id)
660
+ }));
661
+ }
662
+ if (selected?.promotionState === 'promoted') {
663
+ const targets = evidencePaths(paths, selected.sourceId);
664
+ const recorded = JSON.parse(readFileSync(targets.json, 'utf8'));
665
+ promotion = {
666
+ evidenceDigest: recorded.evidenceDigest,
667
+ approvedBy: recorded.promotion?.approvedBy,
668
+ approvedAt: recorded.promotion?.approvedAt,
669
+ promotedCount: Array.isArray(recorded.evidence) ? recorded.evidence.length : 0,
670
+ evidence: [safeRelative(paths.projectRoot, targets.json), safeRelative(paths.projectRoot, targets.markdown)]
671
+ };
672
+ }
673
+ return {
674
+ schema: WORKSPACE_SCHEMA,
675
+ sources,
676
+ selectedSourceId: selected?.sourceId ?? null,
677
+ selectedSource: selected,
678
+ review,
679
+ candidates,
680
+ promotion,
681
+ activePersonas,
682
+ counts: {
683
+ sources: sources.length,
684
+ current: sources.filter((source) => source.freshness === 'current').length,
685
+ stale: sources.filter((source) => source.freshness !== 'current').length
686
+ },
687
+ candidateCounts: Object.fromEntries([...reviewDecisions].map((decision) => [
688
+ decision,
689
+ candidates.filter((candidate) => candidate.disposition?.decision === decision).length
690
+ ])),
691
+ actions: {
692
+ register: true,
693
+ prepare: Boolean(selected && selected.freshness === 'current' && selected.cloudProcessing === 'allowed'),
694
+ review: Boolean(selected && selected.freshness === 'current'),
695
+ promote: Boolean(selected && selected.freshness === 'current' && selected.reviewState === 'reviewed' && selected.promotionState !== 'promoted')
696
+ },
697
+ notices: [
698
+ 'Raw meeting text and absolute source paths are intentionally absent from this workspace.',
699
+ 'Persona engagement is contextual and advisory. A named human owns review and promotion.',
700
+ MEETING_EVIDENCE_DISCLAIMER
701
+ ]
702
+ };
703
+ }